Wie wende ich ORDER BY und LIMIT in Kombination mit einer Aggregatfunktion an?

8

Eine Geige für meine Frage finden Sie unter https://dbfiddle.uk/?rdbms=postgres_10&fiddle=3cd9335fa07565960c1837aa65143685 .

Ich habe ein einfaches Tabellenlayout:

class
person: belongs to a class

Ich möchte alle Klassen auswählen und für jede Klasse möchte ich die ersten beiden Personenkennungen der zugehörigen Personen nach absteigendem Namen sortieren.

Ich habe dies mit der folgenden Abfrage gelöst:

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

Hinweis: Ich hätte eine Korrelationsunterabfrage in der SELECTKlausel verwenden können, aber ich versuche, dies als Teil eines Lernprozesses zu vermeiden.

Wie Sie sehen, bewerbe ich mich order by p.name descan zwei Stellen:

  • in der Unterabfrage
  • in der Aggregatfunktion

Gibt es eine Möglichkeit, dies zu vermeiden? Mein Zug des Unterrichts:

  • Erstens kann ich das order byin der Unterabfrage natürlich nicht entfernen , da dies eine Abfrage ergeben würde, die meine oben angegebene Anforderung nicht erfüllt.

  • Zweitens denke ich, dass die order byFunktion in der Aggregatfunktion nicht ausgelassen werden kann, da die Zeilenreihenfolge der Unterabfrage in der Aggregatfunktion nicht unbedingt beibehalten wird.

Soll ich die Abfrage umschreiben?

Jarius Hebzo
quelle
1
Ich denke, Sie müssen zwei Bestellungen auf die eine oder andere Weise verwenden. Ein anderer Ansatz wäre die Verwendung einer Fensterfunktion wie row_number (), für die jedoch eine eigene Reihenfolge erforderlich ist.
Lennart
Ist (identifier)der Primärschlüssel von class?
Ypercubeᵀᴹ
@ ypercubeᵀᴹ Ja, richtig. Warum ist das so?
Jarius Hebzo

Antworten:

4

Ich bewerbe mich order by p.name descan zwei Stellen ... Gibt es eine Möglichkeit, dies zu vermeiden?

Ja. Aggregieren Sie direkt mit einem ARRAY-Konstruktor in der lateralen Unterabfrage:

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;

Sie brauchen auch nicht GROUP BYin der äußeren auf SELECTdiese Weise. Kürzer, sauberer, schneller.

Ich habe das LEFT JOINdurch eine Ebene ersetzt, CROSS JOINda der ARRAY-Konstruktor immer genau 1 Zeile zurückgibt. (Wie Sie in einem Kommentar betont haben.)

db <> hier fummeln .

Verbunden:

Reihenfolge der Zeilen in Unterabfragen

So adressieren Sie Ihren Kommentar :

Ich habe erfahren, dass die Reihenfolge der Zeilen in einer Unterabfrage in der äußeren Abfrage niemals garantiert bleibt.

Nun, nein. Während der SQL-Standard keine Garantien bietet, gibt es in Postgres begrenzte Garantien . Das Handbuch:

Diese Reihenfolge ist standardmäßig nicht angegeben, kann jedoch durch Schreiben einer ORDER BYKlausel innerhalb des Gesamtaufrufs gesteuert werden , wie in Abschnitt 4.2.7 gezeigt . Alternativ funktioniert normalerweise die Angabe der Eingabewerte aus einer sortierten Unterabfrage. Zum Beispiel:

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

Beachten Sie, dass dieser Ansatz fehlschlagen kann, wenn die äußere Abfrageebene zusätzliche Verarbeitung enthält, z. B. einen Join, da dies dazu führen kann, dass die Ausgabe der Unterabfrage vor der Berechnung des Aggregats neu angeordnet wird.

Wenn Sie in der nächsten Ebene nur Zeilen aggregieren, ist die Reihenfolge positiv garantiert. Ja, was wir dem ARRAY-Konstruktor zuführen , ist auch eine Unterabfrage . Das ist nicht der Punkt. Es würde auch funktionieren mit 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;

Ich erwarte jedoch, dass der ARRAY-Konstruktor für den Fall schneller ist. Sehen:

Erwin Brandstetter
quelle
Das ist ziemlich interessant. Ich habe erfahren, dass die Reihenfolge der Zeilen in einer Unterabfrage in der äußeren Abfrage niemals garantiert bleibt. Warum ist es in diesem speziellen Fall zu korrigieren, anzunehmen, dass die Zeilen in der innersten Unterabfrage in der richtigen Reihenfolge der ARRAY(...)Konstruktion zugeführt werden ?
Jarius Hebzo
Beantworte meine eigene Frage: Dies ist nicht wirklich eine Unterabfrage (wie in SELECT ... FROM (SELECT ... FROM ...)). Dies ist ein SELECTauf einem SELECT: SELECT ARRAY(SELECT ... FROM ...).
Jarius Hebzo
1
Im Zusammenhang damit: dba.stackexchange.com/a/159717/157363
Jarius Hebzo
1
@JariusHebzo: Ich habe ein bisschen hinzugefügt, um das Problem der Zeilenreihenfolge in Unterabfragen zu beheben.
Erwin Brandstetter
2
Wäre es richtig zu sagen, dass wir in beiden Abfragen das left lateral joindurch gerecht ersetzen können lateral join? In Abwesenheit von Personen gibt die erste Abfrage ein leeres Array zurück, die zweite null, oder? Dies widerspricht dem letzten Satz von dba.stackexchange.com/questions/173831/… , aber ich denke, dass Informationen falsch sind? Ich denke, wir müssen einchecken p.persons is not null(bei der ersten Abfrage) oder p.persons != '{}'(bei der zweiten Abfrage), um nur Klassen mit mindestens einer Person auszugeben.
Jarius Hebzo
2

Hier ist eine Alternative, aber sie ist nicht besser als die, die Sie bereits haben:

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;
Lennart
quelle
Dies ist ein interessanter Ansatz, danke dafür. Ich verstehe jetzt Ihren obigen Kommentar, tatsächlich brauchen wir immer zwei Bestellungen, daran führt kein Weg vorbei. Ich denke das beantwortet meine Frage!
Jarius Hebzo
Ich frage mich, wie dies mit einer echten Datenbank funktionieren wird . Der CTE enthält enumerationdie vollständige personTabelle, wenn ich mich nicht irre (da CTEs eine Speicherbarriere in PostgreSQL darstellen). Es ist im Grunde eine In-Memory-Kopie der personTabelle. Dies ist möglicherweise nicht ideal, da wir im Wesentlichen nur einige Zeilen aus dieser Tabelle benötigen (2 für jede Klasse, um genau zu sein). Vielleicht sollten wir ein select * from (...) where n <= 2um die Abfrage herum hinzufügen enumeration(anstatt in der Hauptabfrage)? Auf diese Weise enthält der CTE enumerationnicht mehr die gesamte personTabelle.
Jarius Hebzo
Ich hoffe, das macht Sinn, ich habe Probleme, es in dieser winzigen Schachtel zu erklären. Ich habe es in dieser Geige demonstriert: dbfiddle.uk/…
Jarius Hebzo
1
Es wird wahrscheinlich schlimmer sein als Ihre Anfrage. Ich habe es nur als Denkanstoß hinzugefügt, da Sie anscheinend verschiedene Techniken untersuchen
Lennart
1
Ich weiß das wirklich sehr zu schätzen! Ich versuche in der Tat, mein SQL-Wissen zu erweitern, indem ich verschiedene Dinge ausprobiere, ohne wirklich zu wissen, was ich die ganze Zeit mache. Ihre Antworten sind sehr hilfreich!
Jarius Hebzo