[英]Postgresql - Clean way to insert records if they don't exist, update if they do
這是我的情況。 我有一個表格,其中包含一堆URL和與之關聯的抓取日期。 當我的程序處理URL時,我想插入一個具有爬網日期的新行。 如果URL已存在,我想將爬網日期更新為當前日期時間。 使用MS SQL或Oracle,我可能會使用MERGE命令。 使用mySQL,我可能會使用ON DUPLICATE KEY UPDATE語法。
我可以在我的程序中執行多個查詢,這可能是也可能不是線程安全的。 我可以編寫一個具有各種IF ... ELSE邏輯的SQL函數。 但是,為了嘗試我之前從未使用的Postgres功能,我正在考慮創建一個INSERT規則 - 類似這樣:
CREATE RULE Pages_Upsert AS ON INSERT TO Pages
WHERE EXISTS (SELECT 1 from Pages P where NEW.Url = P.Url)
DO INSTEAD
UPDATE Pages SET LastCrawled = NOW(), Html = NEW.Html WHERE Url = NEW.Url;
這似乎真的很棒。 它可能在“代碼可讀性”的立場上失去一些觀點,因為有人第一次看到我的代碼時必須神奇地了解這個規則,但我想這可以通過良好的代碼注釋和文檔來解決。
這個想法是否有任何其他缺點,或者“你的想法很糟糕,你應該這樣做/這個/方式而不是”評論? 如果重要的話,我在PG 9.0上。
更新 :查詢計划,因為有人想要它:)
"Insert (cost=2.79..2.81 rows=1 width=0)"
" InitPlan 1 (returns $0)"
" -> Seq Scan on pages p (cost=0.00..2.79 rows=1 width=0)"
" Filter: ('http://www.foo.com'::text = lower((url)::text))"
" -> Result (cost=0.00..0.01 rows=1 width=0)"
" One-Time Filter: ($0 IS NOT TRUE)"
""
"Update (cost=2.79..5.46 rows=1 width=111)"
" InitPlan 1 (returns $0)"
" -> Seq Scan on pages p (cost=0.00..2.79 rows=1 width=0)"
" Filter: ('http://www.foo.com'::text = lower((url)::text))"
" -> Result (cost=0.00..2.67 rows=1 width=111)"
" One-Time Filter: $0"
" -> Seq Scan on pages (cost=0.00..2.66 rows=1 width=111)"
" Filter: ((url)::text = 'http://www.foo.com'::text)"
好的,我設法創建了一個測試用例。 結果是即使在新插入時也始終執行更新部分。 COPY似乎繞過了規則系統。 [為清楚起見,我已將此作為單獨的答復]
DROP TABLE pages CASCADE;
CREATE TABLE pages
( url VARCHAR NOT NULL PRIMARY KEY
, html VARCHAR
, last TIMESTAMP
);
INSERT INTO pages(url,html,last) VALUES ('www.example.com://page1' , 'meuk1' , '2001-09-18 23:30:00'::timestamp );
CREATE RULE Pages_Upsert AS ON INSERT TO pages
WHERE EXISTS (SELECT 1 from pages P where NEW.url = P.url)
DO INSTEAD (
UPDATE pages SET html=new.html , last = NOW() WHERE url = NEW.url
);
INSERT INTO pages(url,html,last) VALUES ('www.example.com://page2' , 'meuk2' , '2002-09-18 23:30:00':: timestamp );
INSERT INTO pages(url,html,last) VALUES ('www.example.com://page3' , 'meuk3' , '2003-09-18 23:30:00':: timestamp );
INSERT INTO pages(url,html,last) SELECT pp.url || '/added'::text, pp.html || '.html'::text , pp.last + interval '20 years' FROM pages pp;
COPY pages(url,html,last) FROM STDIN;
www.example.com://pageX stdin 2000-09-18 23:30:00
\.
SELECT * FROM pages;
結果:
url | html | last
-------------------------------+------------+----------------------------
www.example.com://page1 | meuk1 | 2001-09-18 23:30:00
www.example.com://page2 | meuk2 | 2011-09-18 23:48:30.775373
www.example.com://page3 | meuk3 | 2011-09-18 23:48:30.783758
www.example.com://page1/added | meuk1.html | 2011-09-18 23:48:30.792097
www.example.com://page2/added | meuk2.html | 2011-09-18 23:48:30.792097
www.example.com://page3/added | meuk3.html | 2011-09-18 23:48:30.792097
www.example.com://pageX | stdin | 2000-09-18 23:30:00
(7 rows)
更新:只是為了證明它可以做到:
INSERT INTO pages(url,html,last) VALUES ('www.example.com://page1' , 'meuk1' , '2001-09-18 23:30:00'::timestamp );
CREATE VIEW vpages AS (SELECT * from pages);
CREATE RULE Pages_Upsert AS ON INSERT TO vpages
DO INSTEAD (
UPDATE pages p0
SET html=NEW.html , last = NOW() WHERE p0.url = NEW.url
;
INSERT INTO pages (url,html,last)
SELECT NEW.url, NEW.html, NEW.last
WHERE NOT EXISTS ( SELECT * FROM pages p1 WHERE p1.url = NEW.url)
);
CREATE RULE Pages_Indate AS ON UPDATE TO vpages
DO INSTEAD (
INSERT INTO pages (url,html,last)
SELECT NEW.url, NEW.html, NEW.last
WHERE NOT EXISTS ( SELECT * FROM pages p1 WHERE p1.url = OLD.url)
;
UPDATE pages p0
SET html=NEW.html , last = NEW.last WHERE p0.url = NEW.url
;
);
INSERT INTO vpages(url,html,last) VALUES ('www.example.com://page2' , 'meuk2' , '2002-09-18 23:30:00':: timestamp );
INSERT INTO vpages(url,html,last) VALUES ('www.example.com://page3' , 'meuk3' , '2003-09-18 23:30:00':: timestamp );
INSERT INTO vpages(url,html,last) SELECT pp.url || '/added'::text, pp.html || '.html'::text , pp.last + interval '20 years' FROM vpages pp;
UPDATE vpages SET last = last + interval '-10 years' WHERE url = 'www.example.com://page1' ;
-- Copy does NOT work on views
-- COPY vpages(url,html,last) FROM STDIN;
-- www.example.com://pageX stdin 2000-09-18 23:30:00
-- \.
SELECT * FROM vpages;
結果:
INSERT 0 1
INSERT 0 1
INSERT 0 3
UPDATE 1
url | html | last
-------------------------------+------------+---------------------
www.example.com://page2 | meuk2 | 2002-09-18 23:30:00
www.example.com://page3 | meuk3 | 2003-09-18 23:30:00
www.example.com://page1/added | meuk1.html | 2021-09-18 23:30:00
www.example.com://page2/added | meuk2.html | 2022-09-18 23:30:00
www.example.com://page3/added | meuk3.html | 2023-09-18 23:30:00
www.example.com://page1 | meuk1 | 1991-09-18 23:30:00
(6 rows)
該視圖對於防止重寫系統進入遞歸是必要的。 構建DELETE規則留給讀者練習。
一些應該知道它或者非常接近這樣的人的好點;-)
短篇故事:
SERIAL
和BIGSERIAL
? INSERT
和UPDATE
的RETURNING
子句? random()
類的東西? 所有這些事情歸結為這樣一個事實,即規則系統不是行驅動的,而是以你想象不到的方式轉換你的陳述。
做你自己和你的團隊一個忙,並停止使用角色做這樣的事情。
編輯 :您的問題在PostgreSQL社區中得到了很好的討論。 搜索關鍵詞是: MERGE
, UPSERT
。
我不知道這是否過於主觀,但我對你的解決方案的看法是:它都是關於語義的。 當我進行插入時,我期望插入而不是一些奇特的邏輯可能會插入但可能不是。 確實,這就是功能的用途。
首先,我會嘗試檢查程序中的URL,然后選擇是否插入或更新。 如果結果太慢,我會使用一個函數。 如果將其命名為insert_or_update_url
,則會自動獲得一些免費文檔。 重寫規則要求您具有一些隱含的知識,我通常會盡量避免這種情況。
從好的方面來說:如果有人復制數據但忘記規則和功能,你的解決方案可能會默默地破解(但這可能取決於其他約束),但是缺少的功能會尖叫。 不要誤會我的意思,我認為你的解決方案非常有創意和聰明。 對我的口味來說有點太模糊了。
在Postgres文檔中有一個使用簡單函數實現upsert / merge的例子 。
永遠不要使用規則 - 它們是邪惡的。
在規則限定中,您不能引用其他表而不是舊表。 您應該在規則體中執行此操作。 這完全是因為規則只是一種通知重寫系統應該和不應該執行哪些轉換的方法。 規則不是觸發器,為每一行執行,但它們為查詢計划員提供了良好的按摩,並且很好地重寫計划。 來自文檔:
什么是規則資格? 這是一個限制,告訴我們何時應該執行規則的操作,何時不執行。 此限定條件只能引用偽相關NEW和/或OLD,它們基本上表示作為對象給出的關系(但具有特殊含義)。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.