【问题标题】:DELETE records which do not have a match in another table删除在另一个表中没有匹配的记录
【发布时间】:2013-04-04 06:06:59
【问题描述】:

有两个通过 id 链接的表:

item_tbl (id)
link_tbl (item_id)

item_tbl 中有一些记录在link_tbl 中没有匹配的行。计算其数量的选择是:

SELECT COUNT(*)
FROM link_tbl lnk LEFT JOIN item_tbl itm ON lnk.item_id=itm.id
WHERE itm.id IS NULL

我想从link_tbl 中删除那些孤立记录(那些在另一个表中没有匹配的记录),但我能想到的唯一方法是:

DELETE FROM link_tbl lnk
WHERE lnk.item_id NOT IN (SELECT itm.id FROM item_tbl itm)

link_tbl 中有
262,086,253 条记录
3,033,811 中有item_tbl
16,844,347 孤儿记录link_tbl.
服务器有 4GB RAM 和 8 核 CPU。

EXPLAIN DELETE FROM link_tbl lnk
WHERE lnk.item_id NOT IN (SELECT itm.id FROM item_tbl itm)

返回:

Delete on link lnk  (cost=0.00..11395249378057.98 rows=131045918 width=6)
->  Seq Scan on link lnk  (cost=0.00..11395249378057.98 rows=131045918 width=6)
     Filter: (NOT (SubPlan 1))
     SubPlan 1
       ->  Materialize  (cost=0.00..79298.10 rows=3063207 width=4)
             ->  Seq Scan on item itm  (cost=0.00..52016.07 rows=3063207 width=4)

问题是:

  1. 有没有更好的方法来删除link_tbl中的孤立记录?
  2. 上述解释的准确性如何,或者删除这些记录需要多长时间?

    • 编辑:根据 Erwin Brandstetter 的评论修复。
    • 编辑:PostgreSql 版本是 9.1
    • 编辑:postgresql.config 的某些部分
      1. shared_buffers = 368MB
      2. temp_buffers = 32MB
      3. work_mem = 32MB
      4. maintenance_work_mem = 64MB
      5. max_stack_depth = 6MB
      6. fsync = 关闭
      7. synchronous_commit = 关闭
      8. full_page_writes = 关闭
      9. wal_buffers = 16MB
      10. wal_writer_delay = 5000 毫秒
      11. commit_delay = 10
      12. commit_siblings = 10
      13. 有效缓存大小 = 1600MB

分辨率:

谢谢大家的建议,很有帮助。我终于使用了 Erwin Brandstetter https://stackoverflow.com/a/15959896/1331340 建议的删除,但我稍微调整了一下:

DELETE FROM link_tbl lnk
WHERE lnk.item_id BETWEEN 0 AND 10000
  AND lnk.item_id NOT IN (SELECT itm.id FROM item itm
                          WHERE itm.id BETWEEN 0 AND 10000)

我比较了 NOT IN 和 NOT EXISTS 的结果,输出如下,虽然我使用了 COUNT 而不是 DELETE,但我认为应该是相同的(我的意思是为了进行相对比较):

EXPLAIN ANALYZE SELECT COUNT(*) 
FROM link_tbl lnk
WHERE lnk.item_id BETWEEN 0 AND 20000
  AND lnk.item_id NOT IN (SELECT itm.id
                          FROM item_tbl itm
                          WHERE itm.id BETWEEN 0 AND 20000);

QUERY PLAN
Aggregate  (cost=6002667.56..6002667.57 rows=1 width=0) (actual time=226817.086..226817.088 rows=1 loops=1)
->  Seq Scan on link_tbl lnk  (cost=1592.50..5747898.65 rows=101907564 width=0) (actual time=206.029..225289.570 rows=566625 loops=1)
     Filter: ((item_id >= 0) AND (item_id <= 20000) AND (NOT (hashed SubPlan 1)))
     SubPlan 1
       ->  Index Scan using item_tbl_pkey on item_tbl itm  (cost=0.00..1501.95 rows=36221 width=4) (actual time=0.056..99.266 rows=17560 loops=1)
             Index Cond: ((id >= 0) AND (id <= 20000))
Total runtime: 226817.211 ms


EXPLAIN ANALYZE SELECT COUNT(*)
FROM link_tbl lnk WHERE lnk.item_id>0 AND lnk.item_id<20000
  AND NOT EXISTS (SELECT 1 FROM item_tbl itm WHERE itm.id=lnk.item_id);

QUERY PLAN
Aggregate  (cost=8835772.00..8835772.01 rows=1 width=0)
   (actual time=1209235.133..1209235.135 rows=1 loops=1)
->  Hash Anti Join  (cost=102272.16..8835771.99 rows=1 width=0)
   (actual time=19315.170..1207900.612 rows=566534 loops=1)
     Hash Cond: (lnk.item_id = itm.id)
     ->  Seq Scan on link_tbl lnk  (cost=0.00..5091076.55 rows=203815128 width=4) (actual time=0.016..599147.604 rows=200301872 loops=1)
           Filter: ((item_id > 0) AND (item_id < 20000))
     ->  Hash  (cost=52016.07..52016.07 rows=3063207 width=4) (actual time=19313.976..19313.976 rows=3033811 loops=1)
           Buckets: 131072  Batches: 4  Memory Usage: 26672kB
           ->  Seq Scan on item_tbl itm  (cost=0.00..52016.07 rows=3063207 width=4) (actual time=0.013..9274.158 rows=3033811 loops=1)
Total runtime: 1209260.228 ms

NOT EXISTS 慢了 5 倍。

实际删除数据并没有我担心的时间,我可以分5批删除(10000-20000,20000-100000,100000-200000,200000-1000000和1000000-1755441) .一开始我发现了max item_id,我只需要穿过一半的桌子。

当我尝试 NOT IN 或 EXISTS without the range (with select count) 时,它甚至没有完成,我让它在夜间运行,它仍在早上运行。

我想我是在 Wildplasser 的回答 https://stackoverflow.com/a/15988033/1331340 中寻找 DELETE with USING,但为时已晚。

DELETE FROM one o
USING (
    SELECT o2.id
    FROM one o2
    LEFT JOIN two t ON t.one_id = o2.id
    WHERE t.one_id IS NULL
    ) sq
WHERE sq.id = o.id
    ;

【问题讨论】:

  • 我想你会想要不存在:techonthenet.com/sql/delete.php
  • 我以前从未见过如此高的成本,即使在解释明显荒谬的陈述时也是如此。所以我猜想#2的答案是“比任何人都愿意等待的时间要长得多”。除了看看 explain 对 lucas 的建议说了什么之外,我还想在 item_tbl.id 上放置一个索引,以及从朋友那里借更多的 RAM。
  • 我的猜测是要么没有合适的索引/键,要么预期的命中率太高(低熵索引)。也可能是 work_mem 设置得太高,而 random_page_cost 是默认值(:= 等于 sequence_page_cost)
  • 顺便说一句:简单的数学运算:删除/触摸 16M/252M 将导致约 6% 的行被删除。如果分布(你有有效的统计数据吗?)不是太偏,这实际上意味着你需要接触每一页(加上索引)seq scan 可能是一个不错的选择。
  • 请解决您的问题。文字与代码和数字所传达的内容相反。孤儿在哪里?您还应该提供您的 PostgreSQL 版本、有关现有索引的信息以及是否可以在任何地方存在 NULL 值。

标签: sql postgresql exists bigdata sql-delete


【解决方案1】:

我对四个典型查询进行了基准测试,{work_mem, Effective_cache_size, random_page_cost} 的设置不同,这些设置对所选计划的影响最大。我首先使用默认设置进行了“运行”以预热缓存。 注意:测试集足够小,可以让所有需要的页面都出现在缓存中。

测试集

SET search_path=tmp;

/************************/
DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp ;
SET search_path=tmp;

CREATE TABLE one
        ( id SERIAL NOT NULL PRIMARY KEY
        , payload varchar
        );

CREATE TABLE two
        ( id SERIAL NOT NULL PRIMARY KEY
        , one_id INTEGER REFERENCES one
        , payload varchar
        );

INSERT INTO one (payload) SELECT 'Text_' || gs::text FROM generate_series(1,30000) gs;
INSERT INTO two (payload) SELECT 'Text_' || gs::text FROM generate_series(1,30000) gs;


UPDATE two t
SET one_id = o.id
FROM one o
WHERE o.id = t.id
AND random() < 0.1;

INSERT INTO two (one_id,payload) SELECT one_id,payload FROM two;
INSERT INTO two (one_id,payload) SELECT one_id,payload FROM two;
INSERT INTO two (one_id,payload) SELECT one_id,payload FROM two;

VACUUM ANALYZE one;
VACUUM ANALYZE two;
/***************/

查询:

\echo NOT EXISTS()
EXPLAIN ANALYZE
DELETE FROM one o
WHERE NOT EXISTS ( SELECT * FROM two t
        WHERE t.one_id = o.id
        );

\echo NOT IN()
EXPLAIN ANALYZE 
DELETE FROM one o
WHERE o.id NOT IN ( SELECT one_id FROM two t)
        ;

\echo USING (subquery self LEFT JOIN two where NULL)
EXPLAIN ANALYZE
DELETE FROM one o
USING (
        SELECT o2.id
        FROM one o2
        LEFT JOIN two t ON t.one_id = o2.id
        WHERE t.one_id IS NULL
        ) sq
WHERE sq.id = o.id
        ;

\echo USING (subquery self WHERE NOT EXISTS(two)))
EXPLAIN ANALYZE
DELETE FROM one o
USING (
        SELECT o2.id
        FROM one o2
        WHERE NOT EXISTS ( SELECT *
                FROM two t WHERE t.one_id = o2.id
                )
        ) sq
WHERE sq.id = o.id
        ;

结果(总结)

                        NOT EXISTS()    NOT IN()        USING(LEFT JOIN NULL)   USING(NOT EXISTS)
1) rpc=4.0.csz=1M wmm=64        80.358  14389.026       77.620                  72.917
2) rpc=4.0.csz=1M wmm=64000     60.527  69.104          51.851                  51.004
3) rpc=1.5.csz=1M wmm=64        69.804  10758.480       80.402                  77.356
4) rpc=1.5.csz=1M wmm=64000     50.872  69.366          50.763                  53.339
5) rpc=4.0.csz=1G wmm=64        84.117  7625.792        69.790                  69.627
6) rpc=4.0.csz=1G wmm=64000     49.964  67.018          49.968                  49.380
7) rpc=1.5.csz=1G wmm=64        68.567  3650.008        70.283                  69.933
8) rpc=1.5.csz=1G wmm=64000     49.800  67.298          50.116                  50.345

legend: 
rpc := "random_page_cost"
csz := "effective_cache_size"
wmm := "work_mem"

如您所见,NOT IN() 变体对work_mem 的短缺非常敏感。同意,设置 64(KB) 非常低,但这个“或多或少*”对应于大型数据集,也不适合哈希表。

额外:在预热阶段,NOT EXISTS() 查询遭受了极端的 FK 触发器争用。这似乎是与真空守护程序冲突的结果,真空守护程序在表设置后仍处于活动状态。:

PostgreSQL 9.1.2 on x86_64-unknown-linux-gnu, compiled by gcc (Ubuntu/Linaro 4.6.1-9ubuntu3) 4.6.1, 64-bit
NOT EXISTS()
                                                           QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
 Delete on one o  (cost=6736.00..7623.94 rows=27962 width=12) (actual time=80.596..80.596 rows=0 loops=1)
   ->  Hash Anti Join  (cost=6736.00..7623.94 rows=27962 width=12) (actual time=49.174..61.327 rows=27050 loops=1)
         Hash Cond: (o.id = t.one_id)
         ->  Seq Scan on one o  (cost=0.00..463.00 rows=30000 width=10) (actual time=0.003..5.156 rows=30000 loops=1)
         ->  Hash  (cost=3736.00..3736.00 rows=240000 width=10) (actual time=49.121..49.121 rows=23600 loops=1)
               Buckets: 32768  Batches: 1  Memory Usage: 1015kB
               ->  Seq Scan on two t  (cost=0.00..3736.00 rows=240000 width=10) (actual time=0.006..33.790 rows=240000 loops=1)
 Trigger for constraint two_one_id_fkey: time=467720.117 calls=27050
 Total runtime: 467824.652 ms
(9 rows)

【讨论】:

  • 感谢您的努力,我想我正在寻找使用 USING 的 DELETE 但是当您发布答案时,我已经删除了数据。在我编辑的原始问题中查看我的结果。
  • 不客气。整个练习或多或少是为未来的读者准备的。但它受到了我隐藏的议程的启发:IN(subquery) 不如Not EXISTS (correlated subquery) 正如我们所说,我正在测试 300K 记录,但磨合相当慢......顺便说一句:我认为我的测试台更少具体:你的将删除大约 1/16 的行,我的为零。 (受影响(外)键的分布/稀疏性也可能是一个因素)
  • 好的,谢谢。但是:1)它是关于甲骨文的。 2) 13 岁。 3) 这不是真的。 4)检查EXPLAIN(ANALYZE)的输出; NOT EXISTS() 大多转化为反连接,在适当的时候会使用索引,以及哈希表(给定足够的 work_mem)
  • @wildplasser 32MB 的 work_mem 足够了吗?或者更好,如果问的不是太多,也许你可以看看我帖子中的 postgres 配置并提出一些建议?
  • 设置 random_page_cost 较低(~1.5)是一种诱使计划者使用索引的方法。请不要使用索引可能导致更多页被获取;这取决于您的键的分布/分布以及规划者认为您打算命中的行的分数。
【解决方案2】:

首先:你的文字说:

我想从 item_tbl 中删除那些孤立记录。

但是你的代码说:

DELETE FROM <b>link_tbl</b> lnk ...

更新: 在重读 Q 时,我发现您更有可能想要删除 link_tbl 中的孤立行。行数指向那个方向。 @Lucas) 查询在这种情况下是正确的。但恐怕NOT EXISTS 在这种情况下实际上比NOT IN

为了验证我运行了一个测试用例,这与您的设置非常相似。不能让它变得更大,否则 SQLfiddle 会超时。

-> SQLfiddle.

NOT EXISTS 在相反的情况下会更快。 (我也测试过。)EXISTS 更适合测试“多”端。通常,EXISTSNOT EXISTS 可以获得更多收益——无论如何,该表格必须检查整个表格。证明某事不存在比证明某事存在要难得多。这个普遍真理也适用于数据库。

分而治之

此操作适合拆分。特别是如果您有并发事务(但即使没有),我会考虑将 DELETE 拆分为多个切片,以便事务可以在相当长的时间后 COMMIT

类似:

DELETE FROM link_tbl l
WHERE  l.item_id < 1000000
AND    l.item_id NOT IN (SELECT i.id FROM item_tbl i)

然后l.item_id BETWEEN 100001 AND 200000

您无法使用函数自动执行此操作。这会将所有内容都包装到交易中并违背目的。因此,您必须从任何客户端编写脚本。
或者你可以使用 ..

dblink

这个附加模块让您可以在任何数据库中运行单独的事务,包括运行它的那个。这可以通过持久连接来完成,这应该消除大部分连接开销。 有关如何安装它的说明:
How to use (install) dblink in PostgreSQL?

DO 可以完成这项工作(PostgreSQL 9.0 或更高版本)。一次为 50000 个 item_id 运行 100 个 DELETE 命令:

DO
$$
DECLARE
   _sql text;
BEGIN

PERFORM dblink_connect('port=5432 dbname=mydb');  -- your connection parameters

FOR i IN 0 .. 100
LOOP
   _sql := format('
   DELETE FROM link_tbl l
   WHERE  l.item_id BETWEEN %s AND %s
   AND    l.item_id NOT IN (SELECT i.id FROM item_tbl i)'
   , (50000 * i)::text
   , (50000 * (i+1))::text);

   PERFORM  dblink_exec(_sql);
END LOOP;

PERFORM dblink_disconnect();

END
$$

如果脚本应该被中断:dblink_connect 将其执行的内容写入数据库日志,这样您就可以看到已经完成的内容。

【讨论】:

  • 感谢您的努力,我使用了您的删除,但我将范围添加到另一个选择中,没有它太慢了。您可以在我编辑的问题中找到我的 cmets。
  • 对,重复NOT IN子查询中的条件。我首先尝试使用NOT EXISTS,但它没有帮助。没想到后期优化NOT IN
【解决方案3】:

也许是这样的:

DELETE FROM link_tbl lnk
WHERE NOT EXISTS
  ( SELECT 1 FROM item_tbl item WHERE item.id = lnk.item_id );

在处理大量记录时,创建一个临时表,执行INSERT INTO SELECT * FROM ... 然后删除原始表,重命名临时表,然后重新添加索引会更有效...

【讨论】:

  • 虽然我也赞成NOT EXISTS,但恐怕这会产生完全相同的计划。
  • @wildplasser 我无法与 postgresql 交谈,但在 db2 中,我使用 EXISTS 获得了显着提高的性能...此外,这是一个与 sql server 相关的答案,效果相同:stackoverflow.com/a/2065403/516433
  • @wildplasser,虽然查看了您的个人资料,但在 postgresql 方面,您似乎比我更有资格:)
  • 我知道。 (请记住:我也是NOT EISTS 的粉丝!)not exists 是一个原语,并且旧的计划生成器曾经在删除 IN() 的重复(和 NULL)时遇到问题,通常会强制进行额外的排序 -传递子查询的结果。一旦可以检测、分析和合并子查询,哈希就可以解决所有问题。 BTW:我不够资格。我倾向于凭直觉。好像我合格的......
  • @wildplasser:我进行了一些测试。 NOT EXISTSNOT IN 的计划从来都不一样。但是NOT IN 实际上对于检查 1:n 关系的“1”侧更快。我在更新的答案中写了更多内容。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多