web-dev-qa-db-fra.com

Très lente (plus de 12 heures), une grande table rejoint postgres

J'ai du mal à optimiser un simple LEFT JOIN par rapport à deux tables très volumineuses qui ont duré plus de 12 heures et qui ont duré jusqu'à maintenant.

Voici le plan d'exécution:

Gather  (cost=1001.26..11864143.06 rows=8972234 width=133)
  Workers Planned: 7
  ->  Nested Loop Left Join  (cost=1.26..10773657.51 rows=1281748 width=133)
        ->  Parallel Index Scan using var_case_aliquot_aliquot_ind on var_case_aliquot vca  (cost=0.56..464070.21 rows=1281748 width=103)
        ->  Index Scan using genotype_pos_ind on snv_genotypes gt  (cost=0.70..8.01 rows=1 width=65)
              Index Cond: ((vca.chrom = chrom) AND (vca.start = start) AND (vca.end = end) AND ((vca.alt)::text = (alt)::text))
              Filter: (vca.aliquot_barcode = aliquot_barcode)

Voici la requête:

SELECT vca.aliquot_barcode,
    vca.case_barcode,
    vca.gene_symbol,
    vca.variant_classification,
    vca.variant_type,
    vca.chrom,
    int4range(vca.start::integer, vca."end"::integer, '[]'::text) AS pos,
    vca.alt,
    gt.called AS mutect2_call,
    gt.ref_count,
    gt.alt_count,
    gt.read_depth,
    gt.called OR
        CASE
            WHEN (gt.alt_count + gt.ref_count) > 0 THEN (gt.alt_count::numeric / (gt.alt_count + gt.ref_count)::numeric) > 0.20
            ELSE false
        END AS vaf_corrected_call
   FROM analysis.var_case_aliquot vca
     LEFT JOIN analysis.snv_genotypes gt ON vca.aliquot_barcode = gt.aliquot_barcode AND vca.chrom = gt.chrom AND vca.start = gt.start AND vca."end" = gt."end" AND vca.alt::text = gt.alt::text

Les deux tables sont très grandes: vca et gt ont respectivement 9 millions (2 Go) et 1,3 milliard de lignes (346 Go).

J'ai créé la vca (MATERIALIZED VIEW) dans le seul but d'effectuer cette jointure. Il s'agit essentiellement d'une table de jointure avec uniquement les champs requis pour une jointure gauche 1: 1 correspondante, suivie de métadonnées supplémentaires. Tous les champs joints sont correctement indexés, comme vous pouvez le constater dans le plan de requête.

La requête elle-même est assez simple, y at-il quelque chose qui me manque qui pourrait l'accélérer? Je suppose qu'il n'y a pas moyen d'utiliser WHERE à la place?

Y a-t-il quelque chose que je peux modifier dans mes paramètres postgres qui pourrait aider? Actuellement, j'ai les éléments suivants:

shared_buffers = 4096MB
effective_cache_size = 20GB
work_mem = 64MB
maintenance_work_mem = 4096MB
max_wal_size = 4GB
min_wal_size = 128MB
checkpoint_completion_target = 0.9
max_worker_processes = 16
max_parallel_workers_per_gather = 8
max_parallel_workers = 16

MISE À JOUR 12/12: 

Tableau DDL:

CREATE TABLE analysis.snv_genotypes (
    aliquot_barcode character(30) NOT NULL,
    chrom character(2) NOT NULL,
    start bigint NOT NULL,
    "end" bigint NOT NULL,
    alt character varying(510) NOT NULL,
    genotype character(3),
    read_depth integer,
    ref_count integer,
    alt_count integer,
    called boolean
);

ALTER TABLE ONLY analysis.snv_genotypes
    ADD CONSTRAINT genotype_pk PRIMARY KEY (aliquot_barcode, chrom, start, "end", alt);
CREATE INDEX called_ind ON analysis.snv_genotypes USING btree (called);
CREATE INDEX genotype_pos_ind ON analysis.snv_genotypes USING btree (chrom, start, "end", alt);

CREATE MATERIALIZED VIEW analysis.var_case_aliquot AS
 SELECT var_case_aliquot.aliquot_barcode,
    var_case_aliquot.case_barcode,
    var_case_aliquot.chrom,
    var_case_aliquot.start,
    var_case_aliquot."end",
    var_case_aliquot.alt,
    var_case_aliquot.gene_symbol,
    var_case_aliquot.variant_classification,
    var_case_aliquot.variant_type,
    var_case_aliquot.hgvs_p,
    var_case_aliquot.polyphen,
    var_case_aliquot.sift
   FROM var_case_aliquot
  WITH NO DATA;

CREATE INDEX var_case_aliquot_aliquot_ind ON analysis.var_case_aliquot USING btree (aliquot_barcode);
CREATE INDEX var_case_aliquot_pos_ind ON analysis.var_case_aliquot USING btree (chrom, start, "end", alt);

DDL plus complet ici: https://rextester.com/JRJH43442

MISE À JOUR 12/13:

Pour clarifier, j'utilise Postgres 10.5 sur CentOS 7.3 avec 16 cœurs et 32 ​​Go de mémoire. La requête est en cours d'exécution depuis plus de 24 heures sans résultat.

En vérifiant l'état, il semble que wait_event_type est IO. Cela signifie-t-il que la requête est en train de gratter/écrire dans l'espace de travail? Cela pourrait-il expliquer la lenteur?

+------------------+---------------+---------------+---------------+---------------+-----------------+--------------+--------+-------------+--------------+
| application_name | backend_start | xact_start    | query_start   | state_change  | wait_event_type | wait_event   | state  | backend_xid | backend_xmin |
+------------------+---------------+---------------+---------------+---------------+-----------------+--------------+--------+-------------+--------------+
| psql             | 12/12/18 8:42 | 12/12/18 8:42 | 12/12/18 8:42 | 12/12/18 8:42 | IO              | DataFileRead | active | 22135       | 22135        |
+------------------+---------------+---------------+---------------+---------------+-----------------+--------------+--------+-------------+--------------+

J'ai beaucoup de ressources disponibles:

$ free -h
              total        used        free      shared  buff/cache   available
Mem:            31G        722M        210M        5.0G         30G         25G
Swap:          3.7G        626M        3.1G

Je suppose que rendre plus de mémoire disponible pourrait aider? Existe-t-il un moyen d'optimiser les requêtes nécessitant plus de mémoire que ce dont elles disposent?

6
Floris

Du commentaire de ce post:

Votre requête utilise genotype_pos_indet filtre sur aliquot_barcode. Essayez de supprimer (temporairement) genotype_pos_ind et si cela ne fonctionne pas, cherchez comment forcer l'utilisation de l'index.

Votre requête devrait utiliser genotype_pk à la place.

D'après ce que vous avez dit, il pourrait y avoir beaucoup d'enregistrements avec les mêmes valeurs pour aliquot_barcode, chrom, start et end, de sorte que le SGBDR prendra alors beaucoup de temps pour filtrer chaque aliquot_barcode

Et si c'est encore trop long pour vous, vous pouvez essayer ma réponse plus ancienne, que je garderai pour plus de références:



Malheureusement, je ne pourrai pas optimiser votre requête: il y a trop d'éléments à prendre en compte. Construire un résultat avec 9 millions d'enregistrements de 13 champs peut être trop: un échange peut se produire, votre système d'exploitation n'autorisera pas autant d'allocation de mémoire, tout en faisant JOIN, etc.  (écrit avant la vraie réponse ...)

J'avais l'habitude d'optimiser une requête composée d'une quinzaine de tables d'environ 10 millions d'enregistrements. SELECT de cette taille ne serait jamais réalisable dans un délai raisonnable (moins de 10 heures).

Je n'ai pas de SGBDR pour tester ce que je dis. En outre, je n’ai pas utilisé de code SQL depuis six mois: p Trouver la raison pour laquelle cela prend tant de temps (comme vous l’aviez demandé) consomme trop de temps, c’est donc une autre solution au problème initial.


La solution que j'ai adoptée a été de créer une table temporaire:

  1. Créez la table temporaire: tmp_analysis, avec les mêmes champs que votre SELECT + quelques champs d’utilitaire:

Un champ ID (tmp_ID, un gros int), un booléen pour vérifier si l'enregistrement a été mis à jour (tmp_updated) et un horodatage pour vérifier quand il a été mis à jour (tmp_update_time) . Et bien sûr tous les champs, avec les mêmes types de données , à partir de votre SELECT d'origine (de vca et gt)

  1. Insérez tous vos enregistrements de vca:

Utilisez null (ou toute autre valeur par défaut si vous ne le pouvez pas) pour les champs de gt pour le moment. Définissez tmp_updated sur false. Utilisez une simple count() pour la clé primaire.

  1. Mettez à jour tous ces enregistrements avec les champs de gt.

Utilisez une WHERE plutôt qu'une JOIN:

UPDATE tmp_analysis as tmp -- I don't think you need to use a schema to call tmp_analysis
    SET tmp_update = true,
    tmp_update_time = clock_timestamp(),
    tmp.mutect2_call = gt.called
    gt.ref_count,
    gt.alt_count,
    gt.read_depth,
    gt.called = -- ... (your CASE/WHEN/ELSE/END should work here)
FROM 
    analysis.snv_genotypes gt
WHERE --JOIN should work too
    tmp.aliquot_barcode = gt.aliquot_barcode AND 
    tmp.chrom = gt.chrom AND 
    vca.start = gt.start AND 
    tmp."end" = gt."end" AND 
    tmp.alt::text = gt.alt::text

J'ai dit que vous devriez utiliser EXISTS pour des raisons de performances, mais je me suis trompé car je ne pense pas que vous puissiez récupérer des champs à l'intérieur de la condition EXISTS. Il y aurait peut-être un moyen de dire à Postgresql que c'est une relation de un à un, mais je ne suis pas sûr. Quoi qu'il en soit, index 

  1. De toute évidence, SELECT votre table tmp_analysis pour obtenir vos enregistrements!

Quelques notes pour cela:

  1. Si cela prend trop de temps:

Utilisez le champ tmp_ID pour limiter le nombre de mises à jour à 10 000 par exemple et vérifiez le plan d'exécution de la 3ème requête (UPDATE): Vous devriez avoir une analyse complète de la table de la table temporaire et une analyse d'index sur gt (sur genotype_pk). Sinon, vérifiez vos index et cherchez comment forcer l'utilisation de l'index par PGSL. Vous devriez utiliser WHERE tmp_ID < 10000 plutôt que LIMIT 10000. IIRC, LIMIT exécutera la requête entière et vous donnera une partie du résultat.

  1. Si cela prend encore trop de temps:

Segmentez la requête à l'aide de tmp_ID et (comme vous l'avez dit) utilisez une instruction boucle sur la UPDATE pour interroger avec 100 000 enregistrements ou moins à la fois (utilisez à nouveau where tmp_ID < x AND tmp_ID > y). Vérifiez à nouveau le plan d'exécution: l'analyse complète doit être limitée par le tmp_id avant l'analyse d'index. N'oubliez pas d'ajouter un index sur ce fild (s'il ne s'agit pas déjà de la clé primaire).

  1. Si vous devez rappeler plus tard:

Utilisez BEGIN/END TRANSACTION pour encapsuler toutes les requêtes et l'option TEMPORARY TABLE sur CREATE TABLE tmp_analysis pour ne pas avoir à nettoyer tmp_analysis après l'exécution de la requête.

  1. Si vous avez toujours un problème avec les boucles:

Utilisez des transactions dans la boucle et arrêtez-la si elle se fige à nouveau. Ensuite, vous pourrez le restaurer plus tard avec une taille de boucle plus petite.

  1. Si vous voulez réduire un peu le temps d'exécution:

Vous pouvez effectuer les étapes 1 et 2 dans une requête avec un INSERT .. AS .. SELECT, mais je ne me souviens pas comment définir le type de données pour les champs à partir de gt, car ils seront définis sur null. Normalement, cela devrait être un peu plus rapide dans son ensemble.

  1. Si vous êtes curieux:

Et la requête sans boucle prend encore plus de 10 heures, arrêtez-la et consultez tmp_update_time pour voir comment les temps d'exécution évoluent. Cela vous donnera peut-être une idée de la raison pour laquelle la requête d'origine n'a pas fonctionné. Il existe plusieurs options de configuration sur PGSQL pour limiter l'utilisation de RAM, l'utilisation du disque, les threads. Votre système d'exploitation peut mettre ses propres limites et vérifier l'échange de disque, l'utilisation du cache de la CPU, etc. (je pense que vous avez déjà fait cela, mais je n'ai pas vérifié)

1
Asoub