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