web-dev-qa-db-fra.com

Comment appliquer ORDER BY et LIMIT en combinaison avec une fonction d'agrégation?

Un violon pour ma question se trouve sur https://dbfiddle.uk/?rdbms=postgres_10&fiddle=3cd9335fa07565960c1837aa65143685 .

J'ai une disposition de table simple:

class
person: belongs to a class

Je veux sélectionner toutes les classes, et pour chaque classe, je veux que les deux premiers identifiants des personnes appartenant soient triés par nom décroissant.

J'ai résolu cela avec la requête suivante:

select     c.identifier, array_agg(p.identifier order by p.name desc) as persons
from       class as c
left join lateral (
             select   p.identifier, p.name
             from     person as p
             where    p.class_identifier = c.identifier
             order by p.name desc
             limit    2
           ) as p
on         true
group by   c.identifier
order by   c.identifier

Remarque: j'aurais pu utiliser une sous-requête de corrélation dans la clause SELECT, mais j'essaie d'éviter cela dans le cadre d'un processus d'apprentissage.

Comme vous pouvez le voir, j'applique order by p.name desc à deux endroits:

  • dans la sous-requête
  • dans la fonction d'agrégation

Y a-t-il un moyen d'éviter cela? Mon train de réflexion:

  • Tout d'abord, je ne peux évidemment pas supprimer le order by dans la sous-requête, car cela donnerait une requête qui ne répond pas à mes exigences, comme indiqué ci-dessus.

  • Deuxièmement, je pense que le order by dans la fonction d'agrégation ne peut pas être omis, car l'ordre des lignes de la sous-requête n'est pas nécessairement conservé dans la fonction d'agrégation?

Dois-je réécrire la requête?

8
Jarius Hebzo

J'applique order by p.name desc À deux endroits ... Y a-t-il un moyen d'éviter cela?

Oui. Agréger avec un constructeur ARRAY dans la sous-requête latérale directement:

SELECT c.identifier, p.persons
FROM   class c
CROSS  JOIN LATERAL (
   SELECT ARRAY (
      SELECT identifier
      FROM   person
      WHERE  class_identifier = c.identifier
      ORDER  BY name DESC
      LIMIT  2
      ) AS persons
   ) p
ORDER  BY c.identifier;

Vous n'avez pas non plus besoin de GROUP BY Dans le SELECT extérieur de cette façon. Plus court, plus propre, plus rapide.

J'ai remplacé le LEFT JOIN Par un simple CROSS JOIN Car le constructeur ARRAY renvoie toujours exactement 1 ligne. (Comme vous l'avez souligné dans un commentaire.)

db <> violon ici.

En relation:

Ordre des lignes dans les sous-requêtes

Pour adresser votre commentaire :

J'ai appris que l'ordre des lignes d'une sous-requête n'est jamais garanti d'être conservé dans la requête externe.

Et bien non. Bien que le standard SQL n'offre aucune garantie, il existe des garanties limitées dans Postgres. Le manuel:

Cet ordre n'est pas spécifié par défaut, mais peut être contrôlé en écrivant une clause ORDER BY Dans l'appel agrégé, comme indiqué dans Section 4.2.7 . Alternativement, la fourniture des valeurs d'entrée à partir d'une sous-requête triée fonctionne généralement. Par exemple:

SELECT xmlagg(x) FROM (SELECT x FROM test ORDER BY y DESC) AS tab;

Attention, cette approche peut échouer si le niveau de requête externe contient un traitement supplémentaire, tel qu'une jointure, car cela pourrait entraîner la réorganisation de la sortie de la sous-requête avant le calcul de l'agrégat.

Si tout ce que vous faites au niveau suivant est d'agréger des lignes, l'ordre est garanti positivement. Tout oui, ce que nous fournissons au constructeur ARRAY est aussi une sous-requête. Ce n'est pas le propos. Cela fonctionnerait également avec array_agg():

SELECT c.identifier, p.persons
FROM   class c
CROSS  JOIN LATERAL (
   SELECT array_agg(identifier) AS persons
   FROM  (
      SELECT identifier
      FROM   person
      WHERE  class_identifier = c.identifier
      ORDER  BY name DESC
      LIMIT  2
      ) sub
   ) p
ORDER  BY c.identifier;

Mais je m'attends à ce que le constructeur ARRAY soit plus rapide pour le cas. Voir:

4
Erwin Brandstetter

Voici une alternative, mais ce n'est pas mieux que ce que vous avez déjà:

with enumeration (class_identifier, identifier, name, n) as (
    select  p.class_identifier, p.identifier, p.name
         , row_number() over (partition by p.class_identifier 
                              order by p.name desc)
    from     person as p
)
select c.identifier, array_agg(e.identifier order by e.n) as persons
from class as c
left join  enumeration e
    on c.identifier = e.class_identifier
where e.n <= 2
group by   c.identifier
order by   c.identifier;
2
Lennart