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)
質問は次のとおりです。
link_tbl
から削除する方法はありますか?上記の説明はどの程度正確ですか、またはそれらのレコードを削除するのにどのくらい時間がかかりますか?
解決策:
アドバイスありがとうございました。とても参考になりました。私はついに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
;
{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)
_
まず、あなたのテキストは言う:
item_tbl
。からこれらの孤立したレコードを削除したい
しかし、あなたのコードは言う:
DELETE FROM link_tbl lnk ...
更新: Qを再読み込みすると、link_tbl
の孤立した行を削除する可能性が高くなります。行カウントはその方向を指します。 @ Lucas )この場合、クエリは正しいでしょう。しかし、私は恐れています NOT EXISTS
は、この場合NOT IN
より実際には遅いです。
テストケースを実行したことを確認するために、これはリモートでの設定に似ています。それを大きくできなかったか、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
など.
これを関数で自動化することはできません。それはすべてをトランザクションにラップし、目的を無視します。したがって、任意のクライアントからスクリプトを作成する必要があります。
または、..
この追加モジュールを使用すると、実行中のデータベースを含め、どのデータベースでも個別のトランザクションを実行できます。これは、永続的な接続を介して実行できるため、接続オーバーヘッドのほとんどを取り除くことができます。インストール方法:
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ログに書き込むため、すでに何が行われたかを確認できます。
おそらくこれは:
DELETE FROM link_tbl lnk
WHERE NOT EXISTS
( SELECT 1 FROM item_tbl item WHERE item.id = lnk.item_id );
大量のレコードを処理する場合、一時テーブルを作成し、INSERT INTO SELECT * FROM ...
を実行してから元のテーブルを削除し、一時テーブルの名前を変更してから、インデックスを元に戻す方がはるかに効率的です...