SQL-Join: Auswahl der letzten Datensätze in einer Eins-zu-Viele-Beziehung

298

Angenommen, ich habe eine Tabelle mit Kunden und eine Tabelle mit Einkäufen. Jeder Einkauf gehört einem Kunden. Ich möchte eine Liste aller Kunden zusammen mit ihrem letzten Einkauf in einer SELECT-Anweisung erhalten. Was ist die beste Vorgehensweise? Irgendwelche Ratschläge zum Erstellen von Indizes?

Bitte verwenden Sie diese Tabellen- / Spaltennamen in Ihrer Antwort:

  • Kunde: ID, Name
  • Kauf: ID, Kunden-ID, Artikel-ID, Datum

Und wäre es in komplizierteren Situationen (in Bezug auf die Leistung) vorteilhaft, die Datenbank zu denormalisieren, indem der letzte Kauf in die Kundentabelle aufgenommen wird?

Wenn die (Kauf-) ID garantiert nach Datum sortiert ist, können die Aussagen durch die Verwendung von etwas vereinfacht werden LIMIT 1?

netvope
quelle
Ja, es könnte sich lohnen, denormalisiert zu werden (wenn es die Leistung erheblich verbessert, was Sie nur durch Testen beider Versionen herausfinden können). Aber die Nachteile der Denormalisierung sind normalerweise zu vermeiden.
Vince Bowdren
2
Siehe auch
igorw

Antworten:

449

Dies ist ein Beispiel für das greatest-n-per-groupProblem, das regelmäßig bei StackOverflow aufgetreten ist.

So empfehle ich normalerweise, es zu lösen:

SELECT c.*, p1.*
FROM customer c
JOIN purchase p1 ON (c.id = p1.customer_id)
LEFT OUTER JOIN purchase p2 ON (c.id = p2.customer_id AND 
    (p1.date < p2.date OR (p1.date = p2.date AND p1.id < p2.id)))
WHERE p2.id IS NULL;

Erläuterung: Bei einer gegebenen Zeile p1sollte es keine Zeile p2mit demselben Kunden und einem späteren Datum geben (oder bei Krawatten ein späteres id). Wenn wir feststellen, dass dies zutrifft, p1ist dies der letzte Kauf für diesen Kunden.

In Bezug auf Indizes, würde ich eine Verbindung Index in erstellen purchaseüber die Spalten ( customer_id, date, id). Dies kann ermöglichen, dass die äußere Verbindung unter Verwendung eines Abdeckungsindex durchgeführt wird. Stellen Sie sicher, dass Sie auf Ihrer Plattform testen, da die Optimierung von der Implementierung abhängt. Verwenden Sie die Funktionen Ihres RDBMS, um den Optimierungsplan zu analysieren. ZB EXPLAINauf MySQL.


Einige Leute verwenden Unterabfragen anstelle der oben gezeigten Lösung, aber ich finde, dass meine Lösung das Auflösen von Bindungen erleichtert.

Bill Karwin
quelle
3
Günstig im Allgemeinen. Dies hängt jedoch von der Marke der von Ihnen verwendeten Datenbank sowie der Menge und Verteilung der Daten in Ihrer Datenbank ab. Die einzige Möglichkeit, eine genaue Antwort zu erhalten, besteht darin, beide Lösungen anhand Ihrer Daten zu testen.
Bill Karwin
27
Wenn Sie Kunden einbeziehen möchten, die noch nie einen Kauf getätigt haben, ändern Sie JOIN-Kauf p1 ON (c.id = p1.customer_id) in LEFT JOIN-Kauf p1 ON (c.id = p1.customer_id)
GordonM
5
@russds, Sie benötigen eine eindeutige Spalte, mit der Sie das Unentschieden lösen können. Es macht keinen Sinn, zwei identische Zeilen in einer relationalen Datenbank zu haben.
Bill Karwin
6
Was ist der Zweck von "WHERE p2.id IS NULL"?
Clu
3
Diese Lösung funktioniert nur, wenn mehr als 1 Kaufdatensätze vorhanden sind. Wenn es einen 1: 1-Link gibt, funktioniert er NICHT. dort muss es sein "WO (p2.id IST NULL oder p1.id = p2.id)
Bruno Jennrich
126

Sie können dies auch mit einer Unterauswahl versuchen

SELECT  c.*, p.*
FROM    customer c INNER JOIN
        (
            SELECT  customer_id,
                    MAX(date) MaxDate
            FROM    purchase
            GROUP BY customer_id
        ) MaxDates ON c.id = MaxDates.customer_id INNER JOIN
        purchase p ON   MaxDates.customer_id = p.customer_id
                    AND MaxDates.MaxDate = p.date

Die Auswahl sollte allen Kunden und ihrem letzten Kaufdatum beitreten .

Adriaan Stander
quelle
4
Vielen Dank, das hat mich gerade gerettet - diese Lösung scheint praktikabler und wartbarer zu sein als die anderen aufgelisteten + es ist nicht produktspezifisch
Daveo
Wie würde ich dies ändern, wenn ich einen Kunden gewinnen wollte, auch wenn es keine Einkäufe gab?
Clu
3
@clu: Ändern Sie die INNER JOINzu a LEFT OUTER JOIN.
Sasha Chedygov
3
Dies setzt voraus, dass an diesem Tag nur ein Kauf getätigt wird. Wenn es zwei gäbe, würden Sie zwei Ausgabezeilen für einen Kunden erhalten, denke ich?
Artfulrobot
1
@IstiaqueAhmed - Der letzte INNER JOIN nimmt diesen Max-Wert (Datum) und bindet ihn wieder an die Quelltabelle. Ohne diesen Join wären die einzigen Informationen, die Sie aus der purchaseTabelle hätten, das Datum und die customer_id, aber die Abfrage fragt nach allen Feldern aus der Tabelle.
Lachender Vergil
26

Sie haben die Datenbank nicht angegeben. Wenn es sich um eine handelt, die analytische Funktionen ermöglicht, ist es möglicherweise schneller, diesen Ansatz zu verwenden als die GROUP BY-Methode (definitiv schneller in Oracle, höchstwahrscheinlich schneller in den späten SQL Server-Editionen, keine Kenntnis von anderen).

Die Syntax in SQL Server lautet:

SELECT c.*, p.*
FROM customer c INNER JOIN 
     (SELECT RANK() OVER (PARTITION BY customer_id ORDER BY date DESC) r, *
             FROM purchase) p
ON (c.id = p.customer_id)
WHERE p.r = 1
Madalina Dragomir
quelle
10
Dies ist die falsche Antwort auf die Frage, da Sie "RANK ()" anstelle von "ROW_NUMBER ()" verwenden. RANK gibt Ihnen immer noch das gleiche Problem der Krawatten, wenn zwei Käufe genau das gleiche Datum haben. Das macht die Ranking-Funktion; Wenn die Top 2 übereinstimmen, wird beiden der Wert 1 zugewiesen, und dem 3. Datensatz wird der Wert 3 zugewiesen. Bei Row_Number gibt es keine Bindung, sie ist für die gesamte Partition eindeutig.
MikeTeeVee
4
Als ich Bill Karwins Ansatz gegen Madalinas Ansatz hier mit aktivierten Ausführungsplänen unter SQL Server 2008 versuchte, stellte ich fest, dass Bill Karwins Apprach Abfragekosten von 43% hatte, im Gegensatz zu Madalinas Ansatz, der 57% verwendete - also trotz der eleganteren Syntax dieser Antwort würde immer noch Bills Version bevorzugen!
Shawson
26

Ein anderer Ansatz wäre, eine NOT EXISTSBedingung in Ihrer Beitrittsbedingung zu verwenden, um auf spätere Einkäufe zu testen:

SELECT *
FROM customer c
LEFT JOIN purchase p ON (
       c.id = p.customer_id
   AND NOT EXISTS (
     SELECT 1 FROM purchase p1
     WHERE p1.customer_id = c.id
     AND p1.id > p.id
   )
)
Stefan Haberl
quelle
Können Sie den AND NOT EXISTSTeil in einfachen Worten erklären ?
Istiaque Ahmed
Die Unterauswahl prüft nur, ob es eine Zeile mit einer höheren ID gibt. Sie erhalten nur dann eine Zeile in Ihrer Ergebnismenge, wenn keine mit höherer ID gefunden wird. Das sollte der einzigartig höchste sein.
Stefan Haberl
2
Dies ist für mich die am besten lesbare Lösung. Wenn das wichtig ist.
Fguillen
:) Vielen Dank. Ich bemühe mich immer um die am besten lesbare Lösung, denn das ist wichtig.
Stefan Haberl
19

Ich habe diesen Thread als Lösung für mein Problem gefunden.

Aber als ich sie ausprobierte, war die Leistung gering. Unten ist mein Vorschlag für eine bessere Leistung.

With MaxDates as (
SELECT  customer_id,
                MAX(date) MaxDate
        FROM    purchase
        GROUP BY customer_id
)

SELECT  c.*, M.*
FROM    customer c INNER JOIN
        MaxDates as M ON c.id = M.customer_id 

Hoffe das wird hilfreich sein.

Mathee
quelle
um nur 1 zu bekommen, habe ich top 1und ordered it byMaxDatedesc
Roshna Omer
1
Dies ist eine einfache und unkomplizierte Lösung, in MEINEM Fall (viele Kunden, wenige Einkäufe) 10% schneller als die Lösung von @Stefan Haberl und mehr als zehnmal besser als die akzeptierte Antwort
Juraj Bezručka
Toller Vorschlag, Common Table Expressions (CTE) zu verwenden, um dieses Problem zu lösen. Dies hat die Leistung von Abfragen in vielen Situationen dramatisch verbessert.
AdamsTips
Beste Antwort imo, leicht zu lesen, die MAX () -Klausel bietet eine hervorragende Leistung im Vergleich zu ORDER BY + LIMIT 1
mrj
10

Wenn Sie PostgreSQL verwenden, können DISTINCT ONSie die erste Zeile in einer Gruppe suchen.

SELECT customer.*, purchase.*
FROM customer
JOIN (
   SELECT DISTINCT ON (customer_id) *
   FROM purchase
   ORDER BY customer_id, date DESC
) purchase ON purchase.customer_id = customer.id

PostgreSQL Docs - Distinct On

Beachten Sie, dass die DISTINCT ONFelder - hier customer_id- mit den Feldern ganz links im Feld übereinstimmen müssenORDER BY Klausel .

Vorsichtsmaßnahme: Dies ist eine nicht standardmäßige Klausel.

Tate Thurston
quelle
8

Versuchen Sie dies, es wird helfen.

Ich habe dies in meinem Projekt verwendet.

SELECT 
*
FROM
customer c
OUTER APPLY(SELECT top 1 * FROM purchase pi 
WHERE pi.customer_id = c.Id order by pi.Id desc) AS [LastPurchasePrice]
Rahul Murari
quelle
Woher kommt der Alias ​​"p"?
TiagoA
das funktioniert nicht gut .... hat ewig gedauert, wo andere Beispiele hier 2 Sekunden für den Datensatz
gedauert
3

Auf SQLite getestet:

SELECT c.*, p.*, max(p.date)
FROM customer c
LEFT OUTER JOIN purchase p
ON c.id = p.customer_id
GROUP BY c.id

Die max()Aggregatfunktion stellt sicher, dass der letzte Kauf aus jeder Gruppe ausgewählt wird (setzt jedoch voraus, dass die Datumsspalte in einem Format vorliegt, in dem max () den neuesten angibt - was normalerweise der Fall ist). Wenn Sie Einkäufe mit demselben Datum abwickeln möchten, können Sie verwendenmax(p.date, p.id) .

In Bezug auf Indizes würde ich beim Kauf einen Index verwenden (customer_id, date, [alle anderen Kaufspalten, die Sie in Ihrer Auswahl zurückgeben möchten]).

Das LEFT OUTER JOIN(im Gegensatz zu INNER JOIN) stellt sicher, dass Kunden, die noch nie einen Kauf getätigt haben, ebenfalls einbezogen werden.

Kennzeichen
quelle
wird nicht in t-sql ausgeführt, da die Auswahl c. * Spalten enthält, die nicht in der Gruppe nach Klausel enthalten sind
Joel_J
1

Bitte versuchen Sie dies,

SELECT 
c.Id,
c.name,
(SELECT pi.price FROM purchase pi WHERE pi.Id = MAX(p.Id)) AS [LastPurchasePrice]
FROM customer c INNER JOIN purchase p 
ON c.Id = p.customerId 
GROUP BY c.Id,c.name;
Milad Shahbazi
quelle