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