GROUP BY eine Spalte, während in PostgreSQL nach einer anderen sortiert wird


8

Wie kann ich GROUP BYeine Spalte, während nur nach einer anderen sortieren .

Ich versuche Folgendes zu tun:

SELECT dbId,retreivalTime 
    FROM FileItems 
    WHERE sourceSite='something' 
    GROUP BY seriesName 
    ORDER BY retreivalTime DESC 
    LIMIT 100 
    OFFSET 0;

Ich möchte die letzten / n / Elemente aus FileItems in absteigender Reihenfolge auswählen , wobei die Zeilen nach den DISTINCTWerten von gefiltert werden seriesName. Die obige Abfrage ist fehlerhaft ERROR: column "fileitems.dbid" must appear in the GROUP BY clause or be used in an aggregate function. Ich benötige den dbidWert, um dann die Ausgabe dieser Abfrage zu übernehmen, und JOINihn in der Quelltabelle, um den Rest der Spalten abzurufen, die ich nicht war.

Beachten Sie, dass dies im Grunde die Gestalt der folgenden Frage ist, wobei viele der überflüssigen Details aus Gründen der Klarheit entfernt wurden.


Ursprüngliche Frage

Ich habe ein System, das ich von sqlite3 auf PostgreSQL migriere, weil ich sqlite weitgehend entwachsen bin:

    SELECT
            d.dbId,
            d.dlState,
            d.sourceSite,
        [snip a bunch of rows]
            d.note

    FROM FileItems AS d
        JOIN
            ( SELECT dbId
                FROM FileItems
                WHERE sourceSite='{something}'
                GROUP BY seriesName
                ORDER BY MAX(retreivalTime) DESC
                LIMIT 100
                OFFSET 0
            ) AS di
            ON  di.dbId = d.dbId
    ORDER BY d.retreivalTime DESC;

Grundsätzlich möchte ich die letzten n DISTINCTElemente in der Datenbank auswählen , wobei die eindeutige Einschränkung in einer Spalte und die Sortierreihenfolge in einer anderen Spalte liegt.

Leider funktioniert die obige Abfrage, obwohl sie in SQLite einwandfrei funktioniert, in PostgreSQL mit dem Fehler psycopg2.ProgrammingError: column "fileitems.dbid" must appear in the GROUP BY clause or be used in an aggregate function.

Während das Hinzufügen dbIdzur GROUP BY-Klausel das Problem behebt (z. B. GROUP BY seriesName,dbId), bedeutet dies leider, dass die eindeutige Filterung der Abfrageergebnisse nicht mehr funktioniert, da dbides sich um den Datenbankprimärschlüssel handelt und daher alle Werte unterschiedlich sind.

Nach dem Lesen der Postgres-Dokumentation gibt es SELECT DISTINCT ON ({nnn})jedoch, dass die zurückgegebenen Ergebnisse nach sortiert werden müssen {nnn}.

Daher zu tun , was über ich würde wollen SELECT DISTINCT ON, ich Abfrage für alle haben würde DISTINCT {nnn}und ihre MAX(retreivalTime), irgendwie wieder nach retreivalTimeeher dann {nnn}, dann nehmen Sie die 100 größten und Abfrage der gegen den Tisch mit dem Rest der Zeilen zu erhalten, die ich seriesNameIch möchte vermeiden, da die Datenbank ~ 175K Zeilen und ~ 14K unterschiedliche Werte in der Spalte enthält. Ich möchte nur die neuesten 100, und diese Abfrage ist etwas leistungskritisch (ich benötige Abfragezeiten <1/2 Sekunde).

Meine naive Annahme hier ist im Grunde, dass die DB einfach jede Zeile in absteigender Reihenfolge durchlaufen retreivalTimeund einfach anhalten muss, sobald sie LIMITElemente gesehen hat. Eine vollständige Tabellenabfrage ist also nicht ideal, aber ich gebe nicht vor, wirklich zu verstehen, wie die Datenbank ist System optimiert intern, und ich kann dies völlig falsch angehen.

FWIW, ich verwende gelegentlich andere OFFSETWerte, aber lange Abfragezeiten für Fälle, in denen ein Offset> ~ 500 völlig akzeptabel ist. Grundsätzlich OFFSEThandelt es sich um einen beschissenen Paging-Mechanismus, mit dem ich entkommen kann, ohne jeder Verbindung einen Bildlaufcursor zuweisen zu müssen, und ich werde ihn wahrscheinlich irgendwann noch einmal überprüfen.


Ref - Frage, die ich vor einem Monat gestellt habe und die zu dieser Abfrage geführt hat .


Ok, mehr Notizen:

    SELECT
            d.dbId,
            d.dlState,
            d.sourceSite,
        [snip a bunch of rows]
            d.note

    FROM FileItems AS d
        JOIN
            ( SELECT seriesName, MAX(retreivalTime) AS max_retreivalTime
                FROM FileItems
                WHERE sourceSite='{something}'
                GROUP BY seriesName
                ORDER BY max_retreivalTime DESC
                LIMIT %s
                OFFSET %s
            ) AS di
            ON  di.seriesName = d.seriesName AND di.max_retreivalTime = d.retreivalTime
    ORDER BY d.retreivalTime DESC;

Funktioniert für die Abfrage wie beschrieben ordnungsgemäß, aber wenn ich die Klausel entferneGROUP BY , schlägt sie fehl (in meiner Anwendung optional).

psycopg2.ProgrammingError: column "FileItems.seriesname" must appear in the GROUP BY clause or be used in an aggregate function

Ich glaube, ich verstehe grundsätzlich nicht, wie Unterabfragen in PostgreSQL funktionieren. Wo gehe ich falsch? Ich hatte den Eindruck, dass eine Unterabfrage im Grunde nur eine Inline-Funktion ist, bei der die Ergebnisse nur in die Hauptabfrage eingespeist werden.

Antworten:


9

Konsistente Zeilen

Die wichtige Frage, die noch nicht auf Ihrem Radar zu stehen scheint:
Möchten Sie von jedem Satz von Zeilen für denselben seriesNamedie Spalten einer Zeile oder nur beliebige Werte aus mehreren Zeilen (die möglicherweise zusammenpassen oder nicht)?

Ihre Antwort ist letztere, Sie kombinieren das Maximum dbidmit dem Maximum retreivaltime, das aus einer anderen Zeile stammen kann.

Um konsistente Zeilen zu erhalten, verwenden Sie diese DISTINCT ONund wickeln Sie sie in eine Unterabfrage ein, um das Ergebnis anders zu ordnen:

SELECT * FROM (
   SELECT DISTINCT ON (seriesName)
          dbid, seriesName, retreivaltime
   FROM   FileItems
   WHERE  sourceSite = 'mk' 
   ORDER  BY seriesName, retreivaltime DESC NULLS LAST  -- latest retreivaltime
   ) sub
ORDER BY retreivaltime DESC NULLS LAST
LIMIT  100;

Details für DISTINCT ON:

Nebenbei: sollte wohl sein retrievalTime, oder noch besser : retrieval_time. Nicht zitierte gemischte Fallkennungen sind eine häufige Quelle der Verwirrung in Postgres.

Bessere Leistung mit rCTE

Da es sich hier um eine große Tabelle handelt, benötigen wir eine Abfrage, die einen Index verwenden kann. Dies ist bei der obigen Abfrage nicht der Fall (außer für WHERE sourceSite = 'mk').

Bei näherer Betrachtung scheint Ihr Problem ein Sonderfall eines losen Index-Scans zu sein . Postgres unterstützt keine losen Index-Scans von Haus aus, kann jedoch mit einem rekursiven CTE emuliert werden . Im Postgres-Wiki gibt es ein Codebeispiel für den einfachen Fall.

Verwandte Antwort auf SO mit fortgeschritteneren Lösungen, Erklärung, Geige:

Ihr Fall ist jedoch komplexer. Aber ich glaube, ich habe eine Variante gefunden, damit es für Sie funktioniert. Aufbauend auf diesem Index (ohne WHERE sourceSite = 'mk')

CREATE INDEX mi_special_full_idx ON MangaItems
(retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST, dbid)

Oder (mit WHERE sourceSite = 'mk')

CREATE INDEX mi_special_granulated_idx ON MangaItems
(sourceSite, retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST, dbid)

Der erste Index kann für beide Abfragen verwendet werden, ist jedoch mit der zusätzlichen WHERE-Bedingung nicht vollständig effizient. Der zweite Index ist für die erste Abfrage nur sehr begrenzt geeignet. Da Sie beide Varianten der Abfrage haben, sollten Sie beide Indizes erstellen .

Ich habe dbidam Ende hinzugefügt , um nur Index- Scans zuzulassen .

Diese Abfrage mit einem rekursiven CTE verwendet den Index. Ich habe mit Postgres 9.3 getestet und es funktioniert für mich: kein sequentieller Scan, alle Nur-Index- Scans:

WITH RECURSIVE cte AS (
   (
   SELECT dbid, seriesName, retreivaltime, 1 AS rn, ARRAY[seriesName] AS arr
   FROM   MangaItems
   WHERE  sourceSite = 'mk'
   ORDER  BY retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST
   LIMIT  1
   )
   UNION ALL
   SELECT i.dbid, i.seriesName, i.retreivaltime, c.rn + 1, c.arr || i.seriesName
   FROM   cte c
   ,      LATERAL (
      SELECT dbid, seriesName, retreivaltime
      FROM   MangaItems
      WHERE (retreivaltime, seriesName) < (c.retreivaltime, c.seriesName)
      AND    sourceSite = 'mk'  -- repeat condition!
      AND    seriesName <> ALL(c.arr)
      ORDER  BY retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST
      LIMIT  1
      ) i
   WHERE  c.rn < 101
   )
SELECT dbid
FROM   cte
ORDER  BY rn;

Sie müssen enthalten seriesName in ORDER BY, da retreivaltimenicht eindeutig ist. "Fast" einzigartig ist immer noch nicht einzigartig.

Erklären

  • Die nicht rekursive Abfrage beginnt mit der letzten Zeile.

  • Die rekursive Abfrage fügt die nächstletzte Zeile mit einer hinzu seriesName, die noch nicht in der Liste enthalten ist usw., bis wir 100 Zeilen haben.

  • Wesentliche Teile sind die JOINBedingung (b.retreivaltime, b.seriesName) < (c.retreivaltime, c.seriesName)und die ORDER BYKlausel ORDER BY retreivaltime DESC NULLS LAST, seriesName DESC NULLS LAST. Beide stimmen mit der Sortierreihenfolge des Index überein, wodurch die Magie stattfinden kann.

  • Sammeln seriesNamein einem Array, um Duplikate auszuschließen. Die Kosten für b.seriesName <> ALL(c.foo_arr)steigen zunehmend mit der Anzahl der Zeilen, aber für nur 100 Zeilen ist es immer noch billig.

  • Ich komme gerade zurück, dbidwie in den Kommentaren klargestellt.

Alternative mit Teilindizes:

Wir haben uns schon früher mit ähnlichen Problemen befasst. Hier ist eine hochoptimierte Komplettlösung basierend auf Teilindizes und einer Schleifenfunktion:

Wahrscheinlich der schnellste Weg (mit Ausnahme einer materialisierten Ansicht), wenn er richtig gemacht wird. Aber komplexer.

Materialisierte Ansicht

Da Sie nicht viele Schreibvorgänge haben und diese nicht leistungskritisch sind, wie in den Kommentaren angegeben (sollte in der Frage stehen), speichern Sie die obersten n vorberechneten Zeilen in einer materialisierten Ansicht und aktualisieren Sie sie nach relevanten Änderungen an der zugrunde liegende Tabelle. Richten Sie Ihre leistungskritischen Abfragen stattdessen auf die materialisierte Ansicht.

  • Könnte nur ein "dünner" MV der letzten 1000 dbidoder so sein. Verbinden Sie sich in der Abfrage mit der ursprünglichen Tabelle. Zum Beispiel, wenn der Inhalt manchmal aktualisiert wird, die oberen n Zeilen jedoch unverändert bleiben können.

  • Oder ein "fetter" MV mit ganzen Reihen, um zurückzukehren. Noch schneller. Muss natürlich öfter aufgefrischt werden.

Details im Handbuch hier und hier .


5

Ok, ich habe die Dokumente mehr gelesen und jetzt verstehe ich das Problem zumindest ein bisschen besser.

Grundsätzlich gibt es dbidaufgrund der GROUP BY seriesNameAggregation mehrere mögliche Werte für . Bei SQLite und MySQL wählt die DB-Engine anscheinend nur zufällig eine aus (was in meiner Anwendung absolut in Ordnung ist).

PostgreSQL ist jedoch viel konservativer. Wenn Sie also einen zufälligen Wert auswählen, wird ein Fehler ausgegeben.

Eine einfache Möglichkeit, diese Abfrage zum Laufen zu bringen, besteht darin, eine Aggregationsfunktion auf den relevanten Wert anzuwenden :

SELECT MAX(dbid) AS mdbid, seriesName, MAX(retreivaltime) AS mrt
    FROM MangaItems 
    WHERE sourceSite='mk' 
    GROUP BY seriesName
    ORDER BY mrt DESC 
    LIMIT 100 
    OFFSET 0;

Dadurch ist die Abfrageausgabe vollständig qualifiziert, und die Abfrage funktioniert jetzt.


1

Nun, ich habe tatsächlich eine prozedurale Logik außerhalb der Datenbank verwendet, um das zu erreichen, was ich tun wollte.

Grundsätzlich möchte ich 99% der Zeit die letzten 100 200 Ergebnisse. Der Abfrageplaner scheint dies nicht zu optimieren, und wenn der Wert von OFFSETgroß ist, ist mein prozeduraler Filter viel langsamer.

Wie auch immer, ich habe einen benannten Cursor verwendet, um die Zeilen in der Datenbank manuell zu durchlaufen und die Zeilen in Gruppen von einigen hundert abzurufen. Ich filtere sie dann nach Unterscheidbarkeit in meinem Anwendungscode und schließe den Cursor sofort, nachdem ich die Anzahl der gewünschten unterschiedlichen Ergebnisse gesammelt habe.

Der makoCode (im Grunde Python). Es verbleiben noch viele Debug-Anweisungen.

<%def name="fetchMangaItems(flags='', limit=100, offset=0, distinct=False, tableKey=None, seriesName=None)">
    <%
        if distinct and seriesName:
            raise ValueError("Cannot filter for distinct on a single series!")

        if flags:
            raise ValueError("TODO: Implement flag filtering!")

        whereStr, queryAdditionalArgs = buildWhereQuery(tableKey, None, seriesName=seriesName)
        params = tuple(queryAdditionalArgs)


        anonCur = sqlCon.cursor()
        anonCur.execute("BEGIN;")

        cur = sqlCon.cursor(name='test-cursor-1')
        cur.arraysize = 250
        query = '''

            SELECT
                    dbId,
                    dlState,
                    sourceSite,
                    sourceUrl,
                    retreivalTime,
                    sourceId,
                    seriesName,
                    fileName,
                    originName,
                    downloadPath,
                    flags,
                    tags,
                    note

            FROM MangaItems
            {query}
            ORDER BY retreivalTime DESC;'''.format(query=whereStr)

        start = time.time()
        print("time", start)
        print("Query = ", query)
        print("params = ", params)
        print("tableKey = ", tableKey)

        ret = cur.execute(query, params)
        print("Cursor ret = ", ret)
        # for item in cur:
        #   print("Row", item)

        seenItems = []
        rowsBuf = cur.fetchmany()

        rowsRead = 0

        while len(seenItems) < offset:
            if not rowsBuf:
                rowsBuf = cur.fetchmany()
            row = rowsBuf.pop(0)
            rowsRead += 1
            if row[6] not in seenItems or not distinct:
                seenItems.append(row[6])

        retRows = []

        while len(seenItems) < offset+limit:
            if not rowsBuf:
                rowsBuf = cur.fetchmany()
            row = rowsBuf.pop(0)
            rowsRead += 1
            if row[6] not in seenItems or not distinct:
                retRows.append(row)
                seenItems.append(row[6])

        cur.close()
        anonCur.execute("COMMIT;")

        print("duration", time.time()-start)
        print("Rows used", rowsRead)
        print("Query complete!")

        return retRows
    %>

</%def>

Dadurch werden derzeit die neuesten 100 200 verschiedenen Serienelemente in 115 bis 80 Millisekunden abgerufen (kürzere Zeit bei Verwendung einer lokalen Verbindung anstelle eines TCP-Sockets), während ungefähr 1500 Zeilen verarbeitet werden.

Kommen Sie Kommentare:

  • Zeilen werden in Blöcken von 250 gelesen.
  • buildWhereQueryist mein eigener dynamischer Abfrage-Generator. Ja, das ist eine schreckliche Idee. Ja, ich weiß über SQLalchemy et al. Ich habe mein eigenes geschrieben, weil A. dies ein persönliches Projekt ist, von dem ich nicht erwarte, dass es jemals außerhalb meines Heim-LAN verwendet wird, und B. Es ist eine großartige Möglichkeit, SQL zu lernen.
  • Ich kann in Betracht ziehen, zwischen den beiden Abfragemechanismen zu wechseln, da dies vom Wert des Offsets abhängt. Wenn Offset> 1000 ist und ich nach bestimmten Elementen filtere, überschreitet dieser Ansatz anscheinend die Zeit, die für Verfahren wie die in der Antwort von @ ErwinBrandstetter erforderlich ist.
  • @ ErwinBrandstetters Antwort ist immer noch eine viel bessere allgemeine Lösung. Dies ist nur in einem ganz bestimmten Fall besser.
  • Ich musste aus irgendeinem Grund zwei Cursor verwenden. Sie können einen bestimmten Cursor erstellen , wenn Sie in einer Transaktion sind, aber man kann nicht eine Transaktion ohne einen Cursor (Anmerkung - das ist mit Start - autocommitModus ausgeschaltet ). Ich muss einen anonymen Cursor instanziieren, SQL ausgeben (nur a BEGIN, hier), meinen benannten Cursor erstellen, ihn verwenden, schließen und schließlich mit dem anonymen Cursor festschreiben.
  • Dies könnte wahrscheinlich vollständig in PL / pgSQL erfolgen, und das Ergebnis wäre wahrscheinlich noch schneller, aber ich kenne Python viel besser.
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.