PostgreSQL - Travailler avec un tableau de milliers d'éléments

8

Je cherche à sélectionner des lignes en fonction de la présence ou non d'une colonne dans une grande liste de valeurs que je transmets sous forme de tableau d'entiers.

Voici la requête que j'utilise actuellement:

SELECT item_id, other_stuff, ...
FROM (
    SELECT
        -- Partitioned row number as we only want N rows per id
        ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY start_date) AS r,
        item_id, other_stuff, ...
    FROM mytable
    WHERE
        item_id = ANY ($1) -- Integer array
        AND end_date > $2
    ORDER BY item_id ASC, start_date ASC, allowed ASC
) x
WHERE x.r <= 12

Le tableau est structuré comme tel:

    Column     |            Type             | Collation | Nullable | Default 
---------------+-----------------------------+-----------+----------+---------
 item_id       | integer                     |           | not null | 
 allowed       | boolean                     |           | not null | 
 start_date    | timestamp without time zone |           | not null | 
 end_date      | timestamp without time zone |           | not null | 
 ...


 Indexes:
    "idx_dtr_query" btree (item_id, start_date, allowed, end_date)
    ...

J'ai trouvé cet index après avoir essayé différents et exécuté EXPLAINsur la requête. Celui-ci était le plus efficace pour l'interrogation et le tri. Voici l'analyse expliquée de la requête:

Subquery Scan on x  (cost=0.56..368945.41 rows=302230 width=73) (actual time=0.021..276.476 rows=168395 loops=1)
  Filter: (x.r <= 12)
  Rows Removed by Filter: 90275
  ->  WindowAgg  (cost=0.56..357611.80 rows=906689 width=73) (actual time=0.019..248.267 rows=258670 loops=1)
        ->  Index Scan using idx_dtr_query on mytable  (cost=0.56..339478.02 rows=906689 width=73) (actual time=0.013..130.362 rows=258670 loops=1)
              Index Cond: ((item_id = ANY ('{/* 15,000 integers */}'::integer[])) AND (end_date > '2018-03-30 12:08:00'::timestamp without time zone))
Planning time: 30.349 ms
Execution time: 284.619 ms

Le problème est que le tableau int peut contenir jusqu'à 15 000 éléments environ et la requête devient assez lente dans ce cas (environ 800 ms sur mon ordinateur portable, un Dell XPS récent).

Je pensais que le passage du tableau int en tant que paramètre pouvait être lent, et compte tenu de la liste des identifiants pouvant être stockée au préalable dans la base de données, j'ai essayé de le faire. Je les ai stockés dans un tableau dans une autre table et utilisé item_id = ANY (SELECT UNNEST(item_ids) FROM ...), ce qui était plus lent que mon approche actuelle. J'ai également essayé de les stocker ligne par ligne et d'utiliser item_id IN (SELECT item_id FROM ...), ce qui était encore plus lent, même avec uniquement les lignes pertinentes pour mon cas de test dans le tableau.

Existe-t-il une meilleure façon de le faire?

Mise à jour: suite aux commentaires d'Evan , une autre approche que j'ai essayée: chaque élément fait partie de plusieurs groupes, donc au lieu de passer les identifiants des éléments du groupe, j'ai essayé d'ajouter les identifiants de groupe dans mytable:

    Column     |            Type             | Collation | Nullable | Default 
---------------+-----------------------------+-----------+----------+---------
 item_id       | integer                     |           | not null | 
 allowed       | boolean                     |           | not null | 
 start_date    | timestamp without time zone |           | not null | 
 end_date      | timestamp without time zone |           | not null | 
 group_ids     | integer[]                   |           | not null | 
 ...

 Indexes:
    "idx_dtr_query" btree (item_id, start_date, allowed, end_date)
    "idx_dtr_group_ids" gin (group_ids)
    ...

Nouvelle requête ($ 1 est l'ID du groupe cible):

SELECT item_id, other_stuff, ...
FROM (
    SELECT
        -- Partitioned row number as we only want N rows per id
        ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY start_date) AS r,
        item_id, other_stuff, ...
    FROM mytable
    WHERE
        $1 = ANY (group_ids)
        AND end_date > $2
    ORDER BY item_id ASC, start_date ASC, allowed ASC
) x
WHERE x.r <= 12

Expliquez analyser:

Subquery Scan on x  (cost=123356.60..137112.58 rows=131009 width=74) (actual time=811.337..1087.880 rows=172023 loops=1)
  Filter: (x.r <= 12)
  Rows Removed by Filter: 219726
  ->  WindowAgg  (cost=123356.60..132199.73 rows=393028 width=74) (actual time=811.330..1040.121 rows=391749 loops=1)
        ->  Sort  (cost=123356.60..124339.17 rows=393028 width=74) (actual time=811.311..868.127 rows=391749 loops=1)
              Sort Key: item_id, start_date, allowed
              Sort Method: external sort  Disk: 29176kB
              ->  Seq Scan on mytable (cost=0.00..69370.90 rows=393028 width=74) (actual time=0.105..464.126 rows=391749 loops=1)
                    Filter: ((end_date > '2018-04-06 12:00:00'::timestamp without time zone) AND (2928 = ANY (group_ids)))
                    Rows Removed by Filter: 1482567
Planning time: 0.756 ms
Execution time: 1098.348 ms

Il peut y avoir des améliorations à apporter aux index, mais j'ai du mal à comprendre comment postgres les utilise, donc je ne sais pas quoi changer.

Jukurrpa
la source
Combien de lignes dans "mytable"? Combien de valeurs différentes "item_id" là-bas?
Nick
De plus, ne devriez-vous pas avoir de contrainte d'unicité (probablement un index unique non encore défini) dans on item_id dans mytable? ... Modifié: oh, je vois "PARTITION BY item_id", donc cette question se transforme en "Quelle est la vraie clé naturelle pour vos données? Qu'est-ce qui devrait former un index unique là-bas?"
Nick
Environ 12 millions de lignes mytable, avec environ 500k différents item_id. Il n'y a pas de véritable clé unique naturelle pour cette table, ce sont des données qui sont générées automatiquement pour la répétition des événements. Je suppose que le item_id+ start_date+ name(champ non montré ici) pourrait constituer une sorte de clé.
Jukurrpa
Pouvez-vous publier le plan d'exécution que vous obtenez?
Colin 't Hart
Bien sûr, a ajouté l'analyse d'expliquer à la question.
Jukurrpa

Réponses:

1

Existe-t-il une meilleure façon de le faire?

Oui, utilisez une table temporaire. Il n'y a rien de mal à créer une table temporaire indexée lorsque votre requête est aussi folle.

BEGIN;
  CREATE TEMP TABLE myitems ( item_id int PRIMARY KEY );
  INSERT INTO myitems(item_id) VALUES (1), (2); -- and on and on
  CREATE INDEX ON myitems(item_id);
COMMIT;

ANALYZE myitems;

SELECT item_id, other_stuff, ...
FROM (
  SELECT
      -- Partitioned row number as we only want N rows per id
      ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY start_date) AS r,
      item_id, other_stuff, ...
  FROM mytable
  INNER JOIN myitems USING (item_id)
  WHERE end_date > $2
  ORDER BY item_id ASC, start_date ASC, allowed ASC
) x
WHERE x.r <= 12;

Mais encore mieux que ça ...

"500k différent item_id" ... "int array peut contenir jusqu'à 15 000 éléments"

Vous sélectionnez 3% de votre base de données individuellement. Je dois me demander si vous ne feriez pas mieux de créer des groupes / balises, etc. dans le schéma lui-même. Je n'ai jamais personnellement eu à envoyer 15 000 identifiants différents dans une requête.

Evan Carroll
la source
Je viens d'essayer d'utiliser la table temporaire et elle est plus lente, au moins dans le cas des 15 000 identifiants. Quant à la création de groupes dans le schéma lui-même, voulez-vous dire une table avec les identifiants que je passe en argument? J'ai essayé quelque chose comme ça, mais la performance était similaire ou pire que mon approche actuelle. Je mettrai à jour la question avec plus de détails
Jukurrpa
Non je veux dire. Si vous avez 15 000 identifiants normalement, vous stockez quelque chose dans l'ID, comme si l'article est ou non un produit de cuisine, et plutôt que de stocker le group_id qui correspond à "produit de cuisine", vous essayez de trouver tous les produits de cuisine par leurs identifiants. (ce qui est mauvais pour toutes les raisons) Qu'est-ce que ces 15 000 identifiants représentent? Pourquoi n'est-il pas stocké sur la ligne elle-même?
Evan Carroll
Chaque élément appartient à plusieurs groupes (généralement 15 à 20 d'entre eux), j'ai donc essayé de les stocker en tant que tableau int dans mytable, mais je n'ai pas pu trouver comment l'indexer correctement. J'ai mis à jour la question avec tous les détails.
Jukurrpa