Erste Zeile in jeder GROUP BY-Gruppe auswählen?


1323

Wie der Titel schon sagt, möchte ich die erste Zeile jeder Reihe von Zeilen auswählen, die mit a gruppiert sind GROUP BY.

Insbesondere, wenn ich eine purchasesTabelle habe, die so aussieht:

SELECT * FROM purchases;

Mein Output:

id | Kunde | gesamt
--- + ---------- + ------
 1 | Joe | 5
 2 | Sally | 3
 3 | Joe | 2
 4 | Sally | 1

Ich möchte nach iddem größten Kauf ( total) fragen, den jeder getätigt hat customer. Etwas wie das:

SELECT FIRST(id), customer, FIRST(total)
FROM  purchases
GROUP BY customer
ORDER BY total DESC;

Erwartete Ausgabe:

ERSTE (id) | Kunde | ERSTE (insgesamt)
---------- + ---------- + -------------
        1 | Joe | 5
        2 | Sally | 3

Da Sie nur nach jedem größten suchen, warum nicht nachfragen MAX(total)?
Phil294

4
Wenn @ phil294 nach max (total) fragt, wird diese Summe nicht mit dem 'id'-Wert der Zeile verknüpft, in der sie aufgetreten ist.
Gwideman

Antworten:


1116

Unter Oracle 9.2+ (nicht 8i + wie ursprünglich angegeben), SQL Server 2005+, PostgreSQL 8.4+, DB2, Firebird 3.0+, Teradata, Sybase, Vertica:

WITH summary AS (
    SELECT p.id, 
           p.customer, 
           p.total, 
           ROW_NUMBER() OVER(PARTITION BY p.customer 
                                 ORDER BY p.total DESC) AS rk
      FROM PURCHASES p)
SELECT s.*
  FROM summary s
 WHERE s.rk = 1

Unterstützt von jeder Datenbank:

Aber Sie müssen Logik hinzufügen, um Bindungen zu lösen:

  SELECT MIN(x.id),  -- change to MAX if you want the highest
         x.customer, 
         x.total
    FROM PURCHASES x
    JOIN (SELECT p.customer,
                 MAX(total) AS max_total
            FROM PURCHASES p
        GROUP BY p.customer) y ON y.customer = x.customer
                              AND y.max_total = x.total
GROUP BY x.customer, x.total

2
Informix 12.x unterstützt auch Fensterfunktionen (der CTE muss jedoch in eine abgeleitete Tabelle konvertiert werden). Und Firebird 3.0 wird auch Fensterfunktionen unterstützen
a_horse_with_no_name

37
ROW_NUMBER() OVER(PARTITION BY [...])Zusammen mit einigen anderen Optimierungen konnte ich eine Abfrage von 30 Sekunden auf einige Millisekunden reduzieren. Vielen Dank! (PostgreSQL 9.2)
Sam

8
Wenn es mehrere Käufe gibt, von denen der gleiche totalfür einen Kunden gleich hoch ist, gibt die erste Abfrage einen beliebigen Gewinner zurück (abhängig von den Implementierungsdetails; dies idkann sich bei jeder Ausführung ändern!). Normalerweise (nicht immer) möchten Sie eine Zeile pro Kunde, definiert durch zusätzliche Kriterien wie "die mit der kleinsten id". Zum Beheben idan die ORDER BYListe von anhängen row_number(). Dann erhalten Sie das gleiche Ergebnis wie bei der 2. Abfrage, was für diesen Fall sehr ineffizient ist. Außerdem benötigen Sie für jede weitere Spalte eine weitere Unterabfrage.
Erwin Brandstetter

2
Googles BigQuery unterstützt auch den Befehl ROW_NUMBER () der ersten Abfrage. Arbeitete wie ein Zauber für uns
Praxiteles

2
Beachten Sie, dass die erste Version mit der Fensterfunktion ab SQLite Version 3.25.0 funktioniert: sqlite.org/windowfunctions.html#history
brianz

1148

In PostgreSQL ist dies normalerweise einfacher und schneller (weitere Leistungsoptimierung siehe unten):

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

Oder kürzer (wenn nicht so klar) mit Ordnungszahlen der Ausgabespalten:

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

Wenn totalkann NULL sein (wird in beiden Fällen nicht schaden, aber Sie möchten vorhandene Indizes abgleichen ):

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

Hauptpunkte

  • DISTINCT ONist eine PostgreSQL-Erweiterung des Standards (wobei nur DISTINCTdie gesamte SELECTListe definiert ist).

  • Listen Sie eine beliebige Anzahl von Ausdrücken in der DISTINCT ONKlausel auf. Der kombinierte Zeilenwert definiert Duplikate. Das Handbuch:

    Offensichtlich werden zwei Zeilen als unterschiedlich betrachtet, wenn sie sich in mindestens einem Spaltenwert unterscheiden. Nullwerte werden in diesem Vergleich als gleich angesehen.

    Meine kühne Betonung.

  • DISTINCT ONkann mit kombiniert werden ORDER BY. Führende Ausdrücke in ORDER BYmüssen in der Menge der Ausdrücke in enthalten sein DISTINCT ON, aber Sie können die Reihenfolge zwischen diesen frei ändern. Beispiel. Sie können zusätzliche Ausdrücke hinzufügen ORDER BY, um eine bestimmte Zeile aus jeder Gruppe von Peers auszuwählen. Oder, wie es im Handbuch heißt :

    Die DISTINCT ONAusdrücke müssen mit den ORDER BY Ausdrücken ganz links übereinstimmen . Die ORDER BYKlausel enthält normalerweise zusätzliche Ausdrücke, die die gewünschte Priorität von Zeilen innerhalb jeder DISTINCT ONGruppe bestimmen .

    Ich habe idals letzten Punkt hinzugefügt , um die Verbindung zu lösen:
    "Wählen Sie die Zeile mit der kleinsten idaus jeder Gruppe, die die höchste teilt total."

    Um die Ergebnisse so zu ordnen, dass sie nicht mit der Sortierreihenfolge übereinstimmen, die die erste pro Gruppe bestimmt, können Sie die obige Abfrage in einer äußeren Abfrage mit einer anderen verschachteln ORDER BY. Beispiel.

  • Wenn totalNULL sein kann, möchten Sie höchstwahrscheinlich die Zeile mit dem größten Wert ungleich Null. Fügen Sie NULLS LASTwie gezeigt hinzu. Sehen:

  • Die SELECTListe ist nicht durch Ausdrücke in eingeschränkt DISTINCT ONoder ORDER BYin irgendeiner Weise. (Wird im obigen einfachen Fall nicht benötigt):

    • Sie müssen keinen der Ausdrücke in DISTINCT ONoder einfügen ORDER BY.

    • Sie können jeden anderen Ausdruck in die SELECTListe aufnehmen. Dies ist hilfreich, um viel komplexere Abfragen durch Unterabfragen und Aggregat- / Fensterfunktionen zu ersetzen.

  • Ich habe mit Postgres-Versionen 8.3 - 12 getestet. Aber die Funktion ist mindestens seit Version 7.1 vorhanden, also im Grunde immer.

Index

Der perfekte Index für die obige Abfrage wäre ein mehrspaltiger Index , der alle drei Spalten in übereinstimmender Reihenfolge und mit übereinstimmender Sortierreihenfolge umfasst:

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

Kann zu spezialisiert sein. Verwenden Sie es jedoch, wenn die Leseleistung für die jeweilige Abfrage von entscheidender Bedeutung ist. Wenn Sie DESC NULLS LASTin der Abfrage haben, verwenden Sie dasselbe im Index, damit die Sortierreihenfolge übereinstimmt und der Index anwendbar ist.

Effektivität / Leistungsoptimierung

Wägen Sie Kosten und Nutzen ab, bevor Sie für jede Abfrage maßgeschneiderte Indizes erstellen. Das Potenzial des obigen Index hängt weitgehend von der Datenverteilung ab .

Der Index wird verwendet, weil er vorsortierte Daten liefert. In Postgres 9.2 oder höher kann die Abfrage auch nur dann von einem Index-Scan profitieren, wenn der Index kleiner als die zugrunde liegende Tabelle ist. Der Index muss jedoch vollständig gescannt werden.

Benchmark

Ich hatte hier einen einfachen Benchmark, der mittlerweile veraltet ist. Ich habe es in dieser separaten Antwort durch einen detaillierten Benchmark ersetzt .


28
Dies ist eine großartige Antwort für die meisten Datenbankgrößen, aber ich möchte darauf hinweisen, dass ~ Millionen Zeilen DISTINCT ONextrem langsam werden, wenn Sie sich nähern . Die Implementierung sortiert immer die gesamte Tabelle und durchsucht sie nach Duplikaten, wobei alle Indizes ignoriert werden (auch wenn Sie den erforderlichen mehrspaltigen Index erstellt haben). Eine mögliche Lösung finden Sie unter explainextended.com/2009/05/03/postgresql-optimizing-distinct .
Meekohi

14
Die Verwendung von Ordnungszahlen, um "den Code kürzer zu machen", ist eine schreckliche Idee. Wie wäre es, wenn Sie die Spaltennamen belassen, um sie lesbar zu machen?
KOTJMF

13
@KOTJMF: Ich schlage vor, Sie gehen dann mit Ihrer persönlichen Präferenz. Ich zeige beide Möglichkeiten der Bildung. Die Syntaxkürzel kann für lange Ausdrücke in der SELECTListe nützlich sein .
Erwin Brandstetter

1
@jangorecki: Der ursprüngliche Benchmark stammt aus dem Jahr 2011, ich habe das Setup nicht mehr. Aber es war sowieso an der Zeit, Tests mit S. 9.4 und S. 9.5 durchzuführen. Details finden Sie in der hinzugefügten Antwort. . Sie könnten unten einen Kommentar mit dem Ergebnis Ihrer Installation hinzufügen?
Erwin Brandstetter

2
@PirateApp: Nicht von oben. DISTINCT ONist nur gut, um eine Zeile pro Gruppe von Peers zu bekommen.
Erwin Brandstetter

134

Benchmark

Testen der interessantesten Kandidaten mit Postgres 9.4 und 9.5 mit einer halbwegs realistischen Tabelle von 200.000 Zeilen in purchasesund 10.000 unterschiedlichencustomer_id ( durchschnittlich 20 Zeilen pro Kunde ).

Für Postgres 9.5 habe ich einen zweiten Test mit effektiv 86446 verschiedenen Kunden durchgeführt. Siehe unten ( durchschnittlich 2,3 Zeilen pro Kunde ).

Installieren

Haupttisch

CREATE TABLE purchases (
  id          serial
, customer_id int  -- REFERENCES customer
, total       int  -- could be amount of money in Cent
, some_column text -- to make the row bigger, more realistic
);

Ich verwende eine serial(PK-Einschränkung unten hinzugefügt) und eine Ganzzahl, customer_idda dies ein typischeres Setup ist. Wird auch hinzugefügt some_column, um normalerweise mehr Spalten auszugleichen.

Dummy-Daten, PK, Index - eine typische Tabelle enthält auch einige tote Tupel:

INSERT INTO purchases (customer_id, total, some_column)    -- insert 200k rows
SELECT (random() * 10000)::int             AS customer_id  -- 10k customers
     , (random() * random() * 100000)::int AS total     
     , 'note: ' || repeat('x', (random()^2 * random() * random() * 500)::int)
FROM   generate_series(1,200000) g;

ALTER TABLE purchases ADD CONSTRAINT purchases_id_pkey PRIMARY KEY (id);

DELETE FROM purchases WHERE random() > 0.9; -- some dead rows

INSERT INTO purchases (customer_id, total, some_column)
SELECT (random() * 10000)::int             AS customer_id  -- 10k customers
     , (random() * random() * 100000)::int AS total     
     , 'note: ' || repeat('x', (random()^2 * random() * random() * 500)::int)
FROM   generate_series(1,20000) g;  -- add 20k to make it ~ 200k

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

VACUUM ANALYZE purchases;

customer Tabelle - für übergeordnete Abfrage

CREATE TABLE customer AS
SELECT customer_id, 'customer_' || customer_id AS customer
FROM   purchases
GROUP  BY 1
ORDER  BY 1;

ALTER TABLE customer ADD CONSTRAINT customer_customer_id_pkey PRIMARY KEY (customer_id);

VACUUM ANALYZE customer;

In meinem zweiten Test für 9.5 habe ich das gleiche Setup verwendet, aber mit random() * 100000zu generieren customer_id, um nur wenige Zeilen pro zu erhalten customer_id.

Objektgrößen für Tabelle purchases

Mit dieser Abfrage generiert .

               what                | bytes/ct | bytes_pretty | bytes_per_row
-----------------------------------+----------+--------------+---------------
 core_relation_size                | 20496384 | 20 MB        |           102
 visibility_map                    |        0 | 0 bytes      |             0
 free_space_map                    |    24576 | 24 kB        |             0
 table_size_incl_toast             | 20529152 | 20 MB        |           102
 indexes_size                      | 10977280 | 10 MB        |            54
 total_size_incl_toast_and_indexes | 31506432 | 30 MB        |           157
 live_rows_in_text_representation  | 13729802 | 13 MB        |            68
 ------------------------------    |          |              |
 row_count                         |   200045 |              |
 live_tuples                       |   200045 |              |
 dead_tuples                       |    19955 |              |

Abfragen

1. row_number()in CTE ( siehe andere Antwort )

WITH cte AS (
   SELECT id, customer_id, total
        , row_number() OVER(PARTITION BY customer_id ORDER BY total DESC) AS rn
   FROM   purchases
   )
SELECT id, customer_id, total
FROM   cte
WHERE  rn = 1;

2. row_number()in Unterabfrage (meine Optimierung)

SELECT id, customer_id, total
FROM   (
   SELECT id, customer_id, total
        , row_number() OVER(PARTITION BY customer_id ORDER BY total DESC) AS rn
   FROM   purchases
   ) sub
WHERE  rn = 1;

3. DISTINCT ON( siehe andere Antwort )

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

4. rCTE mit LATERALUnterabfrage ( siehe hier )

WITH RECURSIVE cte AS (
   (  -- parentheses required
   SELECT id, customer_id, total
   FROM   purchases
   ORDER  BY customer_id, total DESC
   LIMIT  1
   )
   UNION ALL
   SELECT u.*
   FROM   cte c
   ,      LATERAL (
      SELECT id, customer_id, total
      FROM   purchases
      WHERE  customer_id > c.customer_id  -- lateral reference
      ORDER  BY customer_id, total DESC
      LIMIT  1
      ) u
   )
SELECT id, customer_id, total
FROM   cte
ORDER  BY customer_id;

5. customerTabelle mit LATERAL( siehe hier )

SELECT l.*
FROM   customer c
,      LATERAL (
   SELECT id, customer_id, total
   FROM   purchases
   WHERE  customer_id = c.customer_id  -- lateral reference
   ORDER  BY total DESC
   LIMIT  1
   ) l;

6. array_agg()mit ORDER BY( siehe andere Antwort )

SELECT (array_agg(id ORDER BY total DESC))[1] AS id
     , customer_id
     , max(total) AS total
FROM   purchases
GROUP  BY customer_id;

Ergebnisse

Ausführungszeit für die oben genannten Abfragen mit EXPLAIN ANALYZE(und allen Optionen deaktiviert ), am besten aus 5 Läufen .

Alle Abfragen verwendeten eine Nur - Indexsuche auf purchases2_3c_idx(unter anderen Stufen). Einige von ihnen nur für die kleinere Größe des Index, andere effektiver.

A. Postgres 9.4 mit 200k Reihen und ~ 20 pro customer_id

1. 273.274 ms  
2. 194.572 ms  
3. 111.067 ms  
4.  92.922 ms  
5.  37.679 ms  -- winner
6. 189.495 ms

B. Das gleiche gilt für Postgres 9.5

1. 288.006 ms
2. 223.032 ms  
3. 107.074 ms  
4.  78.032 ms  
5.  33.944 ms  -- winner
6. 211.540 ms  

C. Wie B., jedoch mit ~ 2,3 Zeilen pro customer_id

1. 381.573 ms
2. 311.976 ms
3. 124.074 ms  -- winner
4. 710.631 ms
5. 311.976 ms
6. 421.679 ms

Verwandte Benchmarks

Hier ist ein neuer Test von "ogr" mit 10 Millionen Zeilen und 60.000 einzigartigen "Kunden" auf Postgres 11.5 (Stand: September 2019). Die Ergebnisse stimmen immer noch mit dem überein, was wir bisher gesehen haben:

Ursprünglicher (veralteter) Benchmark von 2011

Ich habe drei Tests mit PostgreSQL 9.1 in einer realen Tabelle mit 65579 Zeilen und einspaltigen btree-Indizes für jede der drei beteiligten Spalten durchgeführt und die beste Ausführungszeit von 5 Läufen genommen.
Vergleich der ersten Abfrage ( A) von @OMGPonies mit der obigen DISTINCT ONLösung ( B):

  1. Wählen Sie die gesamte Tabelle aus. In diesem Fall werden 5958 Zeilen angezeigt.

    A: 567.218 ms
    B: 386.673 ms
    
  2. Verwenden Sie die Bedingung, WHERE customer BETWEEN x AND ydie zu 1000 Zeilen führt.

    A: 249.136 ms
    B:  55.111 ms
    
  3. Wählen Sie einen einzelnen Kunden mit WHERE customer = x.

    A:   0.143 ms
    B:   0.072 ms
    

Der gleiche Test wurde mit dem in der anderen Antwort beschriebenen Index wiederholt

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

1A: 277.953 ms  
1B: 193.547 ms

2A: 249.796 ms -- special index not used  
2B:  28.679 ms

3A:   0.120 ms  
3B:   0.048 ms

5
Vielen Dank für einen tollen Benchmark. Ich habe mich gefragt, ob das Abfragen von Ereignisdaten , bei denen Sie einen Zeitstempel anstelle von total haben , vom neuen BRIN-Index profitieren würde. Dies kann möglicherweise zu einer Beschleunigung für zeitliche Abfragen führen.
Jangorecki

3
@jangorecki: Jede große Tabelle mit physisch sortierten Daten kann von einem BRIN-Index profitieren.
Erwin Brandstetter

@ErwinBrandstetter In den 2. row_number()und 5. customer table with LATERALBeispiele, welche die ID der kleinste wird nicht gewährleistet?
Artem Novikov

@ArtemNovikov: Nichts. Das Ziel ist es, pro customer_id Zeile mit der höchsten abzurufen total. Es ist ein irreführender Zufall in den Testdaten der Frage, dass die idin den ausgewählten Zeilen zufällig auch die kleinste pro ist customer_id.
Erwin Brandstetter

1
@ArtemNovikov: Um nur Index-Scans zuzulassen.
Erwin Brandstetter

55

Das ist üblich Problem, das bereits gut getestete und hochoptimierte Lösungen hat . Persönlich bevorzuge ich die Left Join-Lösung von Bill Karwin (der ursprüngliche Beitrag mit vielen anderen Lösungen ).

Beachten Sie, dass eine Reihe von Lösungen für dieses häufig auftretende Problem überraschenderweise in einer der offiziellsten Quellen, dem MySQL-Handbuch, zu finden sind ! Siehe Beispiele für häufig verwendete Abfragen: Die Zeilen, die das gruppenweise Maximum einer bestimmten Spalte enthalten .


22
Wie ist das MySQL-Handbuch in irgendeiner Weise "offiziell" für Postgres / SQLite-Fragen (ganz zu schweigen von SQL)? Um klar zu sein, ist die DISTINCT ONVersion viel kürzer, einfacher und bietet in Postgres im Allgemeinen eine bessere Leistung als Alternativen mit einem Self- LEFT JOINoder Semi-Anti-Join mit NOT EXISTS. Es ist auch "gut getestet".
Erwin Brandstetter

3
Zusätzlich zu dem, was Erwin geschrieben hat, würde ich sagen, dass die Verwendung einer Fensterfunktion (die heutzutage übliche SQL-Funktionalität ist) fast immer schneller ist als die Verwendung eines Joins mit einer abgeleiteten Tabelle
a_horse_with_no_name

6
Tolle Referenzen. Ich wusste nicht, dass dies das größte Problem pro Gruppe genannt wird. Vielen Dank.
David Mann

Die Frage betrifft nicht das größte n pro Gruppe, sondern das erste n.
Reinierpost

1
In einem Fall mit zwei Auftragsfeldern habe ich versucht, "Left Join Solution von Bill Karwin" eine schlechte Leistung zu erzielen. Siehe meinen Kommentar unten stackoverflow.com/a/8749095/684229
Johnny Wong

30

In Postgres können Sie Folgendes verwenden array_agg:

SELECT  customer,
        (array_agg(id ORDER BY total DESC))[1],
        max(total)
FROM purchases
GROUP BY customer

Dies gibt Ihnen den idgrößten Einkauf jedes Kunden.

Einige Dinge zu beachten:

  • array_aggist eine Aggregatfunktion, mit der es funktioniert GROUP BY.
  • array_aggMit dieser Option können Sie eine Reihenfolge angeben, die nur auf sich selbst beschränkt ist, damit die Struktur der gesamten Abfrage nicht eingeschränkt wird. Es gibt auch eine Syntax zum Sortieren von NULL-Werten, wenn Sie etwas anderes als die Standardeinstellung ausführen müssen.
  • Sobald wir das Array erstellt haben, nehmen wir das erste Element. (Postgres-Arrays sind 1-indiziert, nicht 0-indiziert).
  • Sie können array_aggauf ähnliche Weise für Ihre dritte Ausgabespalte verwenden, dies max(total)ist jedoch einfacher.
  • Im Gegensatz DISTINCT ONdazu array_aggkönnen Sie mit verwenden GROUP BY, falls Sie dies aus anderen Gründen möchten.

14

Die Lösung ist, wie von Erwin erwähnt, aufgrund des Vorhandenseins von SubQs nicht sehr effizient

select * from purchases p1 where total in
(select max(total) from purchases where p1.customer=customer) order by total desc;

Danke, ja, stimme dir zu, der Join zwischen Subq und Outer Query dauert tatsächlich länger. "In" wird hier kein Problem sein, da die Subq nur eine Zeile ergibt. Übrigens, auf welchen Syntaxfehler zeigen Sie?
user2407394

ohh .. verwendet, um "Teradata" .. jetzt bearbeitet .. jedoch ist das Brechen von Bindungen hier nicht erforderlich, da es die höchste Summe für jeden Kunden finden muss ..
user2407394

Sie wissen, dass Sie im Falle eines Unentschieden mehrere Zeilen für einen einzelnen Kunden erhalten? Ob dies wünschenswert ist, hängt von den genauen Anforderungen ab. Normalerweise ist es nicht. Für die vorliegende Frage ist der Titel ziemlich klar.
Erwin Brandstetter

Dies geht nicht aus der Frage hervor, ob derselbe Kunde Kauf = Max für 2 verschiedene IDs hat. Ich denke, wir sollten beide anzeigen.
user2407394

10

Ich benutze diesen Weg (nur postgresql): https://wiki.postgresql.org/wiki/First/last_%28aggregate%29

-- Create a function that always returns the first non-NULL item
CREATE OR REPLACE FUNCTION public.first_agg ( anyelement, anyelement )
RETURNS anyelement LANGUAGE sql IMMUTABLE STRICT AS $$
        SELECT $1;
$$;

-- And then wrap an aggregate around it
CREATE AGGREGATE public.first (
        sfunc    = public.first_agg,
        basetype = anyelement,
        stype    = anyelement
);

-- Create a function that always returns the last non-NULL item
CREATE OR REPLACE FUNCTION public.last_agg ( anyelement, anyelement )
RETURNS anyelement LANGUAGE sql IMMUTABLE STRICT AS $$
        SELECT $2;
$$;

-- And then wrap an aggregate around it
CREATE AGGREGATE public.last (
        sfunc    = public.last_agg,
        basetype = anyelement,
        stype    = anyelement
);

Dann sollte Ihr Beispiel fast so funktionieren wie es ist:

SELECT FIRST(id), customer, FIRST(total)
FROM  purchases
GROUP BY customer
ORDER BY FIRST(total) DESC;

CAVEAT: Die NULL-Zeilen werden ignoriert


Bearbeiten 1 - Verwenden Sie stattdessen die Erweiterung postgres

Jetzt benutze ich diesen Weg: http://pgxn.org/dist/first_last_agg/

So installieren Sie auf Ubuntu 14.04:

apt-get install postgresql-server-dev-9.3 git build-essential -y
git clone git://github.com/wulczer/first_last_agg.git
cd first_last_app
make && sudo make install
psql -c 'create extension first_last_agg'

Es ist eine Postgres-Erweiterung, die Ihnen erste und letzte Funktionen bietet. anscheinend schneller als der obige Weg.


Bearbeiten 2 - Bestellen und Filtern

Wenn Sie Aggregatfunktionen (wie diese) verwenden, können Sie die Ergebnisse ordnen, ohne dass die Daten bereits bestellt werden müssen:

http://www.postgresql.org/docs/current/static/sql-expressions.html#SYNTAX-AGGREGATES

Das äquivalente Beispiel für die Bestellung wäre also etwa:

SELECT first(id order by id), customer, first(total order by id)
  FROM purchases
 GROUP BY customer
 ORDER BY first(total);

Natürlich können Sie bestellen und filtern, wie Sie es für das Aggregat halten. Es ist eine sehr mächtige Syntax.


Verwenden Sie auch diesen benutzerdefinierten Funktionsansatz. Ausreichend universell und einfach. Warum die Dinge komplizieren, ist diese Lösung wesentlich weniger leistungsfähig als andere?
Sergey Shcherbakov

9

Die Abfrage:

SELECT purchases.*
FROM purchases
LEFT JOIN purchases as p 
ON 
  p.customer = purchases.customer 
  AND 
  purchases.total < p.total
WHERE p.total IS NULL

WIE FUNKTIONIERT DAS! (Ich war dort)

Wir möchten sicherstellen, dass wir für jeden Einkauf nur die höchste Summe haben.


Einige theoretische Dinge (überspringen Sie diesen Teil, wenn Sie nur die Abfrage verstehen wollen)

Sei Total eine Funktion T (Kunde, ID), die einen Wert mit dem Namen und der ID zurückgibt. Um zu beweisen, dass die angegebene Summe (T (Kunde, ID)) die höchste ist, müssen wir beweisen, dass wir beides beweisen wollen

  • ∀x T (Kunde, ID)> T (Kunde, x) (diese Summe ist höher als alle anderen Summen für diesen Kunden)

ODER

  • ¬∃x T (Kunde, ID) <T (Kunde, x) (für diesen Kunden gibt es keine höhere Summe)

Der erste Ansatz erfordert, dass wir alle Datensätze für diesen Namen erhalten, die ich nicht wirklich mag.

Der zweite braucht eine kluge Methode, um zu sagen, dass es keinen höheren Datensatz als diesen geben kann.


Zurück zu SQL

Wenn wir die Tabelle mit dem Namen verlassen und die Summe kleiner als die verknüpfte Tabelle ist:

      LEFT JOIN purchases as p 
      ON 
      p.customer = purchases.customer 
      AND 
      purchases.total < p.total

Wir stellen sicher, dass alle Datensätze, die einen anderen Datensatz mit der höheren Summe für denselben Benutzer haben, verbunden werden:

purchases.id, purchases.customer, purchases.total, p.id, p.customer, p.total
1           , Tom           , 200             , 2   , Tom   , 300
2           , Tom           , 300
3           , Bob           , 400             , 4   , Bob   , 500
4           , Bob           , 500
5           , Alice         , 600             , 6   , Alice   , 700
6           , Alice         , 700

Dies hilft uns, bei jedem Einkauf nach der höchsten Gesamtsumme zu filtern, ohne dass eine Gruppierung erforderlich ist:

WHERE p.total IS NULL

purchases.id, purchases.name, purchases.total, p.id, p.name, p.total
2           , Tom           , 300
4           , Bob           , 500
6           , Alice         , 700

Und das ist die Antwort, die wir brauchen.


8

Sehr schnelle Lösung

SELECT a.* 
FROM
    purchases a 
    JOIN ( 
        SELECT customer, min( id ) as id 
        FROM purchases 
        GROUP BY customer 
    ) b USING ( id );

und wirklich sehr schnell, wenn die Tabelle nach id indiziert ist:

create index purchases_id on purchases (id);

Die USING-Klausel ist sehr Standard. Es ist nur so, dass einige kleinere Datenbanksysteme es nicht haben.
Holger Jakobs

2
Dies findet nicht Kundeneinkauf mit der größten Summe
Johnny Wong

7

In SQL Server können Sie dies tun:

SELECT *
FROM (
SELECT ROW_NUMBER()
OVER(PARTITION BY customer
ORDER BY total DESC) AS StRank, *
FROM Purchases) n
WHERE StRank = 1

Explaination: Hier Gruppe von auf der Grundlage von Kunden durchgeführt wird und bestellt es dann insgesamt dann jede solche Gruppe Seriennummern als die Gäste gegeben und wir nehmen aus ersten 1 Kunden , dessen Strank 1


Vielen Dank! Dies funktionierte perfekt und war sehr einfach zu verstehen und umzusetzen.
Ruohola


4

In PostgreSQL besteht eine andere Möglichkeit darin, die first_valueFensterfunktion in Kombination zu verwenden mit SELECT DISTINCT:

select distinct customer_id,
                first_value(row(id, total)) over(partition by customer_id order by total desc, id)
from            purchases;

Ich habe ein Composite erstellt (id, total), sodass beide Werte von demselben Aggregat zurückgegeben werden. Sie können sich natürlich immer first_value()zweimal bewerben .


3

Die akzeptierte Lösung "Unterstützt von jeder Datenbank" von OMG Ponies hat eine gute Geschwindigkeit aus meinem Test.

Hier biete ich einen gleichen Ansatz, aber eine vollständigere und sauberere Lösung für jede Datenbank. Bindungen werden berücksichtigt (vorausgesetzt, Sie möchten nur eine Zeile für jeden Kunden erhalten, sogar mehrere Datensätze für die maximale Gesamtsumme pro Kunde), und andere Kauffelder (z. B. purchase_payment_id) werden für die tatsächlich übereinstimmenden Zeilen in der Kauftabelle ausgewählt.

Unterstützt von jeder Datenbank:

select * from purchase
join (
    select min(id) as id from purchase
    join (
        select customer, max(total) as total from purchase
        group by customer
    ) t1 using (customer, total)
    group by customer
) t2 using (id)
order by customer

Diese Abfrage ist relativ schnell, insbesondere wenn ein zusammengesetzter Index wie (Kunde, Gesamt) in der Kauftabelle vorhanden ist.

Anmerkung:

  1. t1, t2 sind Unterabfrage-Alias, die je nach Datenbank entfernt werden können.

  2. Vorsichtsmaßnahme : Die using (...)Klausel wird derzeit in MS-SQL und Oracle db ab dieser Bearbeitung im Januar 2017 nicht unterstützt. Sie müssen sie selbst auf z on t2.id = purchase.id. B. usw. erweitern. Die USING-Syntax funktioniert in SQLite, MySQL und PostgreSQL.


2

Snowflake / Teradata unterstützt eine QUALIFYKlausel, die wie HAVINGbei Fensterfunktionen funktioniert:

SELECT id, customer, total
FROM PURCHASES
QUALIFY ROW_NUMBER() OVER(PARTITION BY p.customer ORDER BY p.total DESC) = 1

1
  • Wenn Sie eine Zeile (aufgrund Ihrer spezifischen Bedingung) aus der Gruppe der aggregierten Zeilen auswählen möchten.

  • Wenn Sie zusätzlich zu eine andere ( sum/avg) Aggregationsfunktion verwenden möchten max/min. Somit kann man keinen Hinweis mit verwendenDISTINCT ON

Sie können die nächste Unterabfrage verwenden:

SELECT  
    (  
       SELECT **id** FROM t2   
       WHERE id = ANY ( ARRAY_AGG( tf.id ) ) AND amount = MAX( tf.amount )   
    ) id,  
    name,   
    MAX(amount) ma,  
    SUM( ratio )  
FROM t2  tf  
GROUP BY name

Sie können durch eine amount = MAX( tf.amount )beliebige Bedingung mit einer Einschränkung ersetzen : Diese Unterabfrage darf nicht mehr als eine Zeile zurückgeben

Aber wenn Sie solche Dinge tun möchten, suchen Sie wahrscheinlich nach Fensterfunktionen


1

Für SQl Server ist der effizienteste Weg:

with
ids as ( --condition for split table into groups
    select i from (values (9),(12),(17),(18),(19),(20),(22),(21),(23),(10)) as v(i) 
) 
,src as ( 
    select * from yourTable where  <condition> --use this as filter for other conditions
)
,joined as (
    select tops.* from ids 
    cross apply --it`s like for each rows
    (
        select top(1) * 
        from src
        where CommodityId = ids.i 
    ) as tops
)
select * from joined

und vergessen Sie nicht, einen Clustered-Index für verwendete Spalten zu erstellen

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.