PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Optimiser la requête GROUP BY pour récupérer la dernière ligne par utilisateur

Pour de meilleures performances de lecture, vous avez besoin d'un index multicolonne :

CREATE INDEX log_combo_idx
ON log (user_id, log_date DESC NULLS LAST);

Pour effectuer des analyses d'index uniquement possible, ajoutez la colonne payload qui n'est pas nécessaire autrement dans un index de couverture avec le INCLUDE clause (Postgres 11 ou version ultérieure) :

CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST) INCLUDE (payload);

Voir :

  • La couverture des index dans PostgreSQL aide-t-elle les colonnes JOIN ?

Repli pour les anciennes versions :

CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST, payload);

Pourquoi DESC NULLS LAST ?

  • Index inutilisé dans la requête de plage de dates

Pour quelques lignes par user_id ou petits tableaux DISTINCT ON est généralement le plus rapide et le plus simple :

  • Sélectionner la première ligne de chaque groupe GROUP BY ?

Pour beaucoup lignes par user_id une analyse par saut d'index (ou analyse d'index lâche ) est (beaucoup) plus efficace. Ce n'est pas implémenté jusqu'à Postgres 12 - des travaux sont en cours pour Postgres 14. Mais il existe des moyens de l'émuler efficacement.

Les expressions de table communes nécessitent Postgres 8.4+ .
LATERAL nécessite Postgres 9.3+ .
Les solutions suivantes vont au-delà de ce qui est couvert dans le Postgres Wiki .

1. Pas de tableau séparé avec des utilisateurs uniques

Avec un users distinct tableau, solutions en 2. ci-dessous sont généralement plus simples et plus rapides. Passez devant.

1a. CTE récursif avec LATERAL rejoindre

WITH RECURSIVE cte AS (
   (                                -- parentheses required
   SELECT user_id, log_date, payload
   FROM   log
   WHERE  log_date <= :mydate
   ORDER  BY user_id, log_date DESC NULLS LAST
   LIMIT  1
   )
   UNION ALL
   SELECT l.*
   FROM   cte c
   CROSS  JOIN LATERAL (
      SELECT l.user_id, l.log_date, l.payload
      FROM   log l
      WHERE  l.user_id > c.user_id  -- lateral reference
      AND    log_date <= :mydate    -- repeat condition
      ORDER  BY l.user_id, l.log_date DESC NULLS LAST
      LIMIT  1
      ) l
   )
TABLE  cte
ORDER  BY user_id;

C'est simple pour récupérer des colonnes arbitraires et probablement mieux dans Postgres actuel. Plus d'explications au chapitre 2a. ci-dessous.

1b. CTE récursif avec sous-requête corrélée

WITH RECURSIVE cte AS (
   (                                           -- parentheses required
   SELECT l AS my_row                          -- whole row
   FROM   log l
   WHERE  log_date <= :mydate
   ORDER  BY user_id, log_date DESC NULLS LAST
   LIMIT  1
   )
   UNION ALL
   SELECT (SELECT l                            -- whole row
           FROM   log l
           WHERE  l.user_id > (c.my_row).user_id
           AND    l.log_date <= :mydate        -- repeat condition
           ORDER  BY l.user_id, l.log_date DESC NULLS LAST
           LIMIT  1)
   FROM   cte c
   WHERE  (c.my_row).user_id IS NOT NULL       -- note parentheses
   )
SELECT (my_row).*                              -- decompose row
FROM   cte
WHERE  (my_row).user_id IS NOT NULL
ORDER  BY (my_row).user_id;

Pratique pour récupérer une seule colonne ou la ligne entière . L'exemple utilise le type de ligne entier de la table. D'autres variantes sont possibles.

Pour affirmer qu'une ligne a été trouvée dans l'itération précédente, testez une seule colonne NOT NULL (comme la clé primaire).

Plus d'explications pour cette requête dans le chapitre 2b. ci-dessous.

Connexe :

  • Interroger les N dernières lignes associées par ligne
  • GROUPER PAR une colonne, tout en triant par une autre dans PostgreSQL

2. Avec des users distincts tableau

La disposition du tableau importe peu tant qu'il y a exactement une ligne par user_id pertinent est garanti. Exemple :

CREATE TABLE users (
   user_id  serial PRIMARY KEY
 , username text NOT NULL
);

Idéalement, la table est physiquement triée en synchronisation avec le log table. Voir :

  • Optimiser la plage de requêtes d'horodatage Postgres

Ou il est suffisamment petit (faible cardinalité) pour que cela n'ait guère d'importance. Sinon, le tri des lignes dans la requête peut aider à optimiser davantage les performances. Voir l'ajout de Gang Liang. Si l'ordre de tri physique des users la table correspond à l'index sur log , cela peut ne pas être pertinent.

2a. LATERAL rejoindre

SELECT u.user_id, l.log_date, l.payload
FROM   users u
CROSS  JOIN LATERAL (
   SELECT l.log_date, l.payload
   FROM   log l
   WHERE  l.user_id = u.user_id         -- lateral reference
   AND    l.log_date <= :mydate
   ORDER  BY l.log_date DESC NULLS LAST
   LIMIT  1
   ) l;

JOIN LATERAL permet de référencer FROM précédent éléments au même niveau de requête. Voir :

  • Quelle est la différence entre LATERAL JOIN et une sous-requête dans PostgreSQL ?

Résultats dans une recherche d'index (seulement) par utilisateur.

Ne renvoie aucune ligne pour les utilisateurs manquants dans le users table. Généralement, une clé étrangère l'application de contraintes d'intégrité référentielle exclurait cela.

De plus, aucune ligne pour les utilisateurs sans entrée correspondante dans log - conforme à la question initiale. Pour conserver ces utilisateurs dans le résultat, utilisez LEFT JOIN LATERAL ... ON true au lieu de CROSS JOIN LATERAL :

  • Appeler plusieurs fois une fonction renvoyant un ensemble avec un argument de tableau

Utilisez LIMIT n au lieu de LIMIT 1 pour récupérer plusieurs lignes (mais pas tous) par utilisateur.

En fait, tous font la même chose :

JOIN LATERAL ... ON true
CROSS JOIN LATERAL ...
, LATERAL ...

Le dernier a cependant une priorité inférieure. JOIN explicite se lie avant la virgule. Cette différence subtile peut avoir de l'importance avec plus de tables de jointure. Voir :

  • "référence non valide à l'entrée de la clause FROM pour la table" dans la requête Postgres

2b. Sous-requête corrélée

Bon choix pour récupérer une seule colonne à partir d'une ligne unique . Exemple de code :

  • Optimiser la requête maximale par groupe

La même chose est possible pour plusieurs colonnes , mais vous avez besoin de plus d'intelligence :

CREATE TEMP TABLE combo (log_date date, payload int);

SELECT user_id, (combo1).*              -- note parentheses
FROM (
   SELECT u.user_id
        , (SELECT (l.log_date, l.payload)::combo
           FROM   log l
           WHERE  l.user_id = u.user_id
           AND    l.log_date <= :mydate
           ORDER  BY l.log_date DESC NULLS LAST
           LIMIT  1) AS combo1
   FROM   users u
   ) sub;

Comme LEFT JOIN LATERAL ci-dessus, cette variante inclut tous utilisateurs, même sans entrées dans le log . Vous obtenez NULL pour combo1 , que vous pouvez facilement filtrer avec un WHERE clause dans la requête externe si nécessaire.
Nitpick :dans la requête externe, vous ne pouvez pas distinguer si la sous-requête n'a pas trouvé de ligne ou si toutes les valeurs de colonne sont NULL - même résultat. Vous avez besoin d'un NOT NULL colonne dans la sous-requête pour éviter cette ambiguïté.

Une sous-requête corrélée ne peut renvoyer qu'une valeur unique . Vous pouvez envelopper plusieurs colonnes dans un type composite. Mais pour le décomposer plus tard, Postgres exige un type composite bien connu. Les enregistrements anonymes ne peuvent être décomposés qu'en fournissant une liste de définition de colonne.
Utilisez un type enregistré comme le type de ligne d'une table existante. Ou enregistrez un type composite explicitement (et de manière permanente) avec CREATE TYPE . Ou créez une table temporaire (supprimée automatiquement à la fin de la session) pour enregistrer temporairement son type de ligne. Syntaxe de diffusion :(log_date, payload)::combo

Enfin, nous ne voulons pas décomposer combo1 au même niveau de requête. En raison d'une faiblesse du planificateur de requêtes, cela évaluerait la sous-requête une fois pour chaque colonne (toujours vrai dans Postgres 12). Au lieu de cela, faites-en une sous-requête et décomposez-la dans la requête externe.

Connexe :

  • Obtenir les valeurs de la première et de la dernière ligne par groupe

Démonstration des 4 requêtes avec 100 000 entrées de journal et 1 000 utilisateurs :
db<>violez ici - pg 11
Vieux sqlfiddle