【问题标题】:Postgresql - Clean way to insert records if they don't exist, update if they doPostgresql - 如果记录不存在则插入记录的干净方法,如果存在则更新
【发布时间】:2011-11-19 19:13:22
【问题描述】:

这是我的情况。我有一张包含一堆 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;

这似乎真的很好用。它可能会失去一些分数 “代码可读性”的观点,因为有人在看我的代码 第一次必须神奇地知道这个规则,但我 猜测可以通过良好的代码注释来解决 文档。

这个想法是否还有其他缺点,或者可能是“你的想法 糟透了,你应该这样做 /this/ 方式”评论?我在 PG 9.0 如果 这很重要。

UPDATE:查询计划,因为有人想要它:)

"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)"

【问题讨论】:

  • 请将生成的查询计划(针对目标表上的 INSERT 操作)添加到帖子中。
  • 顺便说一句,页面上的 lower(url) 上实际上有一个 INDEX - 我认为它正在进行顺序扫描,因为目前表中只有 4 行,并且规划者认为这样做更快查看 4 行而不是命中索引。只是猜测。
  • 谢谢。我得到的差不多。更新部分总是发生。如果您执行实际的 INSERT INTO pages(...) VALUES (...),则 lastcrawled 的值将被 now() 覆盖,即使在插入之前该键不存在。

标签: database postgresql rules upsert


【解决方案1】:

好的,我设法创建了一个测试用例。结果是始终执行更新部分,即使是在新插入时也是如此。 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 规则的构造留给读者作为练习。

【讨论】:

  • 很多关于这个线程的好反馈,但将这个标记为答案,因为您投入了大量工作。
  • 谢谢。我最近调查了这些东西,所以我得到了大部分方便。结论是:测试一下。测试所有的角盒。如果您诱使重写器使用错误的范围表条目和/或合并子查询或子句,那么“SELECT ... from tab INSERT into tab()”可能是一个真正的阻碍。解释是你的朋友。
【解决方案2】:

一些应该知道或非常接近这样的人的人的优点;-)

What are PostgreSQL RULEs good for?

短篇小说:

  • 这些规则是否适用于 SERIALBIGSERIAL
  • 这些规则是否适用于INSERTUPDATERETURNING 子句?
  • 这些规则是否适用于 random() 之类的内容?

所有这些都归结为这样一个事实,即规则系统不是行驱动的,而是以一种您从未想象过的方式转换您的语句。

帮自己和你的队友一个忙,不要再用角色来做类似的事情了。

编辑:您的问题在 PostgreSQL 社区中得到了很好的讨论。搜索关键字为:MERGEUPSERT

【讨论】:

    【解决方案3】:

    Postgres 文档中有一个example of implementing upsert / merge using simple function

    永远不要使用规则——它们是邪恶的。

    【讨论】:

    • 我同意“不要使用规则”作为经验法则。但它们本身并不邪恶;他们只是很棘手。很难做对。魔鬼在细节中。这是学习理解查询计划的好方法;-]
    • @wildplasser - 他们是 GOTO 级别的恶魔。如果你以某种方式做对了,95% 的其他开发人员都不会。几乎所有使用规则实现的东西都可以使用触发器或简单的函数来实现更清晰、更容易理解和更便携。我说的是邪恶的。
    • 达到 99%。问题是大多数开发人员都考虑“一次一行”处理,这与触发器很好地匹配。将规则视为“类固醇的触发器”是这种破坏范式的结果。扩展您的 GOTO 隐喻:OP 中的规则限定类似于 C 程序中的“void main()”。这完全是错误的,即使它碰巧在某些特定情况下起作用。我因为提到它而被否决的事实支持你的 95% 规则。我休息一下。
    【解决方案4】:

    我不知道这是否过于主观,但我对您的解决方案的看法是:这完全与语义有关。当我进行插入时,我期望插入而不是一些可能会插入但可能不会插入的奇特逻辑。事实上,这就是函数的用途。

    首先,我会尝试检查程序中的 URL,然后选择是插入还是更新。如果结果太慢,我会使用一个函数。如果您将其命名为insert_or_update_url,您将自动免费获得一些文档。重写规则要求你有一些隐含的知识,我通常会尽量避免。

    有利的一面:如果有人复制了数据但忘记了规则和函数,您的解决方案可能会默默地中断(但这可能取决于其他约束),但丢失的函数会尖叫。不要误会我的意思,我认为您的解决方案非常有创意和聪明。对我的口味来说有点太晦涩了。

    【讨论】:

    • 是的,我有点同意。如果我的程序的多个实例同时运行,则首先检查该行是否存在将太慢并且不安全。 PG 功能可能会解决此问题并更快。表模式禁止相同的 URL 存在两次,因此复制数据不是问题,而且这些数据本质上是瞬态的,因此不是非常重要。也许有一天,PG 会有一个 UPSERT 能力,这不会是一个问题。
    【解决方案5】:

    您不能在规则限定中引用除旧表和新表之外的其他表。 您应该改为在 规则正文 中执行此操作。 这都是因为规则只是一种通知重写系统它应该和不应该执行哪些转换的方法。规则不是触发器,对每一行都执行,但它们给查询计划者一个很好的按摩,并很好地要求它重写计划。 来自文档:

    什么是规则限定?这是一个限制,它告诉何时应该执行规则的操作,何时不执行。此限定只能引用伪关系 NEW 和/或 OLD,它们基本上表示作为对象给出的关系(但具有特殊含义)。

    【讨论】:

    • 您的“WHERE EXISTS (SELECT 1 from Pages P where NEW.Url = P.Url)”资格无效。你试过了吗?
    • 我刚刚测试过了。它被接受并且似乎有效。对不起。但根据文档,它不应该工作。可能是虽然可以,但是遇到更复杂的查询(比如insert into .. select from ...,或者self-joins)会产生错误的计划
    • 我认为普遍的共识是“是的,这至少在我的具体情况下会起作用”但是“这很hacky,你在玩火,很难阅读,而且规则很糟糕,嗯,凯“.. 这是一个很好的借口来玩弄我以前没有使用过的 PG 功能,但我认为这不是长期的正确解决方案..
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-02
    • 2010-12-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-23
    相关资源
    最近更新 更多