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

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

DISTINCT ON est généralement le plus simple et le plus rapide pour cela dans PostgreSQL .
(Pour l'optimisation des performances pour certaines charges de travail, voir ci-dessous.)

SELECT DISTINCT ON (customer)
       id, customer, total
FROM   purchases
ORDER  BY customer, total DESC, id;

Ou plus court (sinon aussi clair) avec des nombres ordinaux de colonnes de sortie :

SELECT DISTINCT ON (2)
       id, customer, total
FROM   purchases
ORDER  BY 2, 3 DESC, 1;

Si total peut être NULL (cela ne fera pas de mal de toute façon, mais vous voudrez faire correspondre les index existants) :

...
ORDER  BY customer, total DESC NULLS LAST, id;

Points majeurs

DISTINCT ON est une extension PostgreSQL du standard (où seul DISTINCT dans l'ensemble SELECT liste est définie).

Lister n'importe quel nombre d'expressions dans le DISTINCT ON clause, la valeur de ligne combinée définit les doublons. Le manuel :

Évidemment, deux lignes sont considérées comme distinctes si elles diffèrent par au moins une valeur de colonne. Les valeurs nulles sont considérées comme égales dans cette comparaison.

J'insiste sur moi.

DISTINCT ON peut être combiné avec ORDER BY . Expressions principales dans ORDER BY doit être dans l'ensemble d'expressions dans DISTINCT ON , mais vous pouvez réorganiser librement l'ordre parmi ceux-ci. Exemple.
Vous pouvez ajouter des éléments supplémentaires expressions à ORDER BY pour choisir une ligne particulière dans chaque groupe de pairs. Ou, comme le dit le manuel :

Le DISTINCT ON les expressions doivent correspondre au ORDER BY le plus à gauche expressions). Le ORDER BY la clause contiendra normalement des expressions supplémentaires qui déterminent la priorité souhaitée des lignes dans chaque DISTINCT ON groupe.

J'ai ajouté id comme dernier élément à départager :
"Choisissez la ligne avec le plus petit id de chaque groupe partageant le total le plus élevé ."

Pour trier les résultats d'une manière qui n'est pas d'accord avec l'ordre de tri déterminant le premier par groupe, vous pouvez imbriquer la requête ci-dessus dans une requête externe avec un autre ORDER BY . Exemple.

Si total peut être NULL, vous probablement voulez la ligne avec la plus grande valeur non nulle. Ajouter NULLS LAST comme démontré. Voir :

  • Trier par colonne ASC, mais les valeurs NULL en premier ?

Le SELECT liste n'est pas contraint par des expressions dans DISTINCT ON ou ORDER BY de quelque manière que. (Non nécessaire dans le cas simple ci-dessus) :

  • Vous n'êtes pas obligé inclure l'une des expressions dans DISTINCT ON ou ORDER BY .

  • Vous pouvez inclure toute autre expression dans le SELECT liste. Ceci est essentiel pour remplacer des requêtes beaucoup plus complexes par des sous-requêtes et des fonctions d'agrégation/fenêtre.

J'ai testé avec les versions 8.3 à 13 de Postgres. Mais la fonctionnalité existe au moins depuis la version 7.1, donc en gros toujours.

Index

Le parfait l'index pour la requête ci-dessus serait un index multi-colonnes couvrant les trois colonnes dans l'ordre correspondant et avec l'ordre de tri correspondant :

CREATE INDEX purchases_3c_idx ON purchases (customer, total DESC, id);

Peut-être trop spécialisé. Mais utilisez-le si les performances de lecture pour la requête particulière sont cruciales. Si vous avez DESC NULLS LAST dans la requête, utilisez la même chose dans l'index afin que l'ordre de tri corresponde et que l'index soit applicable.

Efficacité / Optimisation des performances

Évaluez les coûts et les avantages avant de créer des index personnalisés pour chaque requête. Le potentiel de l'indice ci-dessus dépend en grande partie de la distribution des données .

L'index est utilisé car il fournit des données pré-triées. Dans Postgres 9.2 ou version ultérieure, la requête peut également bénéficier d'un balayage d'index uniquement si l'index est plus petit que la table sous-jacente. L'index doit cependant être scanné dans son intégralité.

Pour peu lignes par client (cardinalité élevée dans la colonne customer ), c'est très efficace. Encore plus si vous avez de toute façon besoin d'une sortie triée. L'avantage diminue avec un nombre croissant de lignes par client.
Idéalement, vous avez suffisamment de work_mem pour traiter l'étape de tri impliquée dans la RAM et ne pas se répandre sur le disque. Mais généralement en définissant work_mem aussi élevée peut avoir des effets indésirables. Considérez SET LOCAL pour les requêtes exceptionnellement volumineuses. Trouvez combien vous avez besoin avec EXPLAIN ANALYZE . Mention de "Disque : " dans l'étape de tri indique le besoin d'en savoir plus :

  • Paramètre de configuration work_mem dans PostgreSQL sous Linux
  • Optimiser les requêtes simples à l'aide de la date et du texte ORDER BY

Pour beaucoup lignes par client (faible cardinalité dans la colonne customer ), une analyse d'index lâche (alias "ignorer l'analyse") serait (beaucoup) plus efficace, mais cela n'est pas implémenté jusqu'à Postgres 14. (Une implémentation pour les analyses d'index uniquement est en cours de développement pour Postgres 15. Voir ici et ici.)
Pour maintenant, il existe des techniques de requête plus rapides pour se substituer à cela. En particulier si vous avez une table séparée contenant des clients uniques, ce qui est le cas d'utilisation typique. Mais aussi si vous ne le faites pas :

  • SELECT DISTINCT est plus lent que prévu sur ma table dans PostgreSQL
  • Optimiser la requête GROUP BY pour récupérer la dernière ligne par utilisateur
  • Optimiser la requête maximale par groupe
  • Interroger les N dernières lignes associées par ligne

Repères

Voir réponse séparée.