web-dev-qa-db-ja.com

別のテーブルで一致しないレコードを削除する

IDでリンクされた2つのテーブルがあります。

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)

がある
262,086,253link_tblのレコード
3,033,811in item_tbl
16,844,347link_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 = 32 MB
      4. maintenance_work_mem = 64 MB
      5. max_stack_depth = 6MB
      6. fsync =オフ
      7. synchronous_commit = off
      8. full_page_writes = off
      9. wal_buffers = 16MB
      10. wal_writer_delay = 5000ms
      11. commit_delay = 10
      12. commit_siblings = 10
      13. effective_cache_size = 1600MB

解決策:

アドバイスありがとうございました。とても参考になりました。私はついにErwin Brandstetterの助言による削除 https://stackoverflow.com/a/15959896/133134 を使用しましたが、少し調整しました:

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の結果を比較したところ、出力は以下のようになりましたが、DELETEの代わりにCOUNTを使用しましたが、これは同じはずです(つまり、相対的な比較のためです)。

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)で削除することができました。最初に、最大のitem_idを見つけ、テーブルの半分を通過するだけで済みました。

範囲を指定せずに(選択カウントを使用して)NOT INまたはEXISTSを試したところ、完了しませんでした。夜間に実行させても、午前中に実行されていました。

Wildplasserの回答からUSINGを使用したDELETEを探していたと思います https://stackoverflow.com/a/15988033/133134 が遅すぎました。

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
    ;
15
miloxe

{work_mem、effective_cache_size、random_page_cost}の設定を変えて、4つの典型的なクエリをベンチマークしました。これらの設定は、選択したプランに最も大きな影響を与えます。最初に、デフォルト設定で「実行」を実行して、キャッシュをウォームしました。注:テストセットは、必要なすべてのページがキャッシュに存在できるように十分に小さいです。

テストセット

_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)
_
17
wildplasser

まず、あなたのテキストは言う:

item_tblからこれらの孤立したレコードを削除したい

しかし、あなたのコードは言う:

DELETE FROM link_tbl lnk ...

更新: Qを再読み込みすると、link_tblの孤立した行を削除する可能性が高くなります。行カウントはその方​​向を指します。 @ Lucas )この場合、クエリは正しいでしょう。しかし、私は恐れています NOT EXISTS は、この場合NOT INより実際には遅いです。

テストケースを実行したことを確認するために、これはリモートでの設定に似ています。それを大きくできなかったか、SQLfiddleがタイムアウトになりました。

-> SQLfiddle

NOT EXISTSは、逆の場合に高速です。 (私もテストしました。)EXISTSは、「多」側のテストに適しています。そして一般的に、NOT EXISTSを使用するよりも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

この追加モジュールを使用すると、実行中のデータベースを含め、どのデータベースでも個別のトランザクションを実行できます。これは、永続的な接続を介して実行できるため、接続オーバーヘッドのほとんどを取り除くことができます。インストール方法:
PostgreSQLでdblinkを使用(インストール)する方法?

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は、何が実行されたかをDBログに書き込むため、すでに何が行われたかを確認できます。

7

おそらくこれは:

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

大量のレコードを処理する場合、一時テーブルを作成し、INSERT INTO SELECT * FROM ...を実行してから元のテーブルを削除し、一時テーブルの名前を変更してから、インデックスを元に戻す方がはるかに効率的です...

5
Lucas