web-dev-qa-db-fra.com

SUPPRIMER les enregistrements qui n'ont pas de correspondance dans une autre table

Il existe deux tables liées par un identifiant:

item_tbl (id)
link_tbl (item_id)

Il y a quelques enregistrements dans item_tbl qui n'ont pas de lignes correspondantes dans link_tbl. Une sélection qui compterait leur montant serait:

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

Je voudrais supprimer ces enregistrements orphelins (ceux qui n'ont pas de correspondance dans l'autre table) de link_tbl mais la seule façon dont je pouvais penser était:

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

Il y a
262 086 253 enregistrements dans link_tbl
3 033 811 en item_tbl
16 844 347 Enregistrements orphelins dans link_tbl.
Le serveur dispose de 4 Go RAM et processeur 8 cœurs.

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

Retour:

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)

Les questions sont:

  1. Existe-t-il un meilleur moyen de supprimer les enregistrements orphelins de link_tbl?
  2. Quelle est la précision de l'explication ci-dessus, ou combien de temps cela pourrait prendre pour supprimer ces enregistrements?

    • Edit: corrigé selon le commentaire d'Erwin Brandstetter.
    • Edit: la version PostgreSql est 9.1
    • Edit: certaines parties de postgresql.config
      1. shared_buffers = 368MB
      2. temp_buffers = 32 Mo
      3. work_mem = 32 Mo
      4. maintenance_work_mem = 64 Mo
      5. max_stack_depth = 6 Mo
      6. fsync = off
      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 = 1600 Mo

Résolution:

Merci à tous pour vos conseils, cela a été très utile. J'ai finalement utilisé la suppression conseillée par Erwin Brandstetter https://stackoverflow.com/a/15959896/133134 mais je l'ai légèrement modifiée:

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)

J'ai comparé les résultats de NOT IN et NOT EXISTS et la sortie est ci-dessous, bien que j'aie utilisé COUNT au lieu de DELETE qui devrait être le même (je veux dire par souci de comparaison relative):

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 était 5 fois plus lent.

La suppression réelle des données n'a pas pris aussi longtemps que j'étais inquiet, j'ai pu les supprimer en 5 lots (10000-20000,20000-100000,100000-200000,200000-1000000 et 1000000-1755441). Au début, j'ai découvert max item_id et je n'avais qu'à parcourir la moitié du tableau.

Lorsque j'ai essayé NOT IN ou EXISTS sans la plage (avec un nombre sélectionné), il n'a même pas fini, je l'ai laissé fonctionner pendant la nuit et il fonctionnait toujours le matin.

Je pense que je cherchais DELETE avec USING de la réponse de wildplasser https://stackoverflow.com/a/15988033/133134 mais c'est arrivé trop tard.

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

J'ai comparé quatre requêtes typiques, avec des paramètres différents pour {work_mem, effective_cache_size, random_page_cost}, ces paramètres ont la plus grande influence sur le plan sélectionné. J'ai d'abord fait un "rodage" avec mes paramètres par défaut pour réchauffer le cache. Remarque: l'ensemble de tests est suffisamment petit pour permettre à toutes les pages nécessaires d'être présentes dans le cache.

L'ensemble de test

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;
/***************/

Les requêtes:

\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
        ;

Le résultat (résumé)

                        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"

Comme vous pouvez le voir, la variante NOT IN() est très sensible à la pénurie de work_mem. D'accord, le paramètre 64 (Ko) est très bas, mais cela `plus ou moins * correspond à de grands ensembles de données, qui ne rentrent pas non plus dans les tables de hachage.

EXTRA: pendant la phase de préchauffage, la requête NOT EXISTS() a souffert d'une contention extrême du déclencheur FK. Cela semble être le résultat d'un conflit avec le démon à vide, qui est toujours actif après la configuration de la table .:

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

Tout d'abord: votre texte dit:

Je souhaite supprimer ces enregistrements orphelins de item_tbl.

Mais votre code dit:

DELETE FROM link_tbl lnk ...

Mise à jour: En relisant le Q, je trouve plus probable que vous souhaitiez supprimer des lignes orphelines dans link_tbl. Le nombre de lignes pointe dans cette direction. @ Lucas ) la requête serait correcte dans ce cas. Mais j'ai peur, NOT EXISTS est en fait plus lent que NOT IN dans ce cas.

Pour vérifier que j'ai exécuté un scénario de test, cela ressemble à distance à votre configuration. Impossible de l'agrandir beaucoup, ou SQLfiddle se retrouverait dans un délai d'attente.

-> SQLfiddle .

NOT EXISTS serait plus rapide pour le cas inversé. (J'ai aussi testé cela.) EXISTS est mieux adapté pour tester le côté "many". Et généralement, il y a plus à gagner avec EXISTS qu'avec NOT EXISTS - ce formulaire doit quand même vérifier l'ensemble du tableau. Il est beaucoup plus difficile de prouver quelque chose n'existe pas que de prouver que quelque chose existe. Cette vérité universelle s'applique également aux bases de données.

Diviser et conquérir

Cette opération est apte à être fractionnée. Surtout si vous avez des transactions simultanées (mais même sans), j'envisagerais de diviser le DELETE en plusieurs tranches, afin que la transaction puisse COMMIT après un laps de temps décent.

Quelque chose comme:

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

Ensuite l.item_id BETWEEN 100001 AND 200000, etc.

Vous ne pouvez pas automatiser cela avec une fonction. Cela emballerait tout dans une transaction et défierait le but. Vous devez donc l'écrire à partir de n'importe quel client.
Ou vous pouvez utiliser ..

dblink

Ce module supplémentaire vous permet d'exécuter des transactions distinctes dans n'importe quelle base de données, y compris celle dans laquelle il est exécuté. Et cela peut être fait via une connexion persistante, ce qui devrait supprimer la plupart des frais généraux de connexion. Pour savoir comment l'installer:
Comment utiliser (installer) dblink dans PostgreSQL?

DO ferait le travail (PostgreSQL 9.0 ou supérieur). Exécution de 100 commandes DELETE pour 50000 item_id à la fois:

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
$$

Si le script devait être interrompu: dblink_connect écrit dans le journal de base de données ce qu'il a exécuté, de sorte que vous voyez ce qui est déjà fait.

7
Erwin Brandstetter

Peut-être ceci:

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

Lorsque vous traitez un grand nombre d'enregistrements, il peut être beaucoup plus efficace de créer une table temporaire, exécutez INSERT INTO SELECT * FROM ... puis déposez la table d'origine, renommez la table temporaire, puis rajoutez vos index ...

5
Lucas