478 Stimmen

SQL Join: Auswählen der letzten Datensätze in einer eins-zu-viele Beziehung

Angenommen, ich habe eine Tabelle mit Kunden und eine Tabelle mit Einkäufen. Jeder Einkauf gehört zu einem Kunden. Ich möchte eine Liste aller Kunden zusammen mit ihrem letzten Kauf 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, customer_id, item_id, date

Und in komplizierteren Situationen, wäre es (leistungsbedingt) vorteilhaft, die Datenbank zu denormalisieren, indem man den letzten Kauf in die Kundentabelle aufnimmt?

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

2 Stimmen

Ja, es könnte sich lohnen, zu denormalisieren (wenn es die Leistung erheblich verbessert, was Sie nur durch Testen beider Versionen herausfinden können). Aber die Nachteile der Denormalisierung sind normalerweise das Vermeiden wert.

3 Stimmen

674voto

Bill Karwin Punkte 493880

Dies ist ein Beispiel für das Größte-N-pro-Gruppe-Problem, das regelmäßig auf StackOverflow auftritt.

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;

Erklärung: Für eine Zeile p1 sollte es keine Zeile p2 mit demselben Kunden und einem späteren Datum geben (oder im Falle von Gleichständen eine spätere id). Wenn das zutrifft, ist p1 der aktuellste Kauf für diesen Kunden.

Was die Indizes betrifft, würde ich einen zusammengesetzten Index in purchase über die Spalten (customer_id, date, id) erstellen. Dadurch könnte der äußere Join mithilfe eines deckenden Index durchgeführt werden. Testen Sie auf Ihrer Plattform, denn die Optimierung hängt von der Implementierung ab. Nutzen Sie die Funktionen Ihres RDBMS, um den Optimierungsplan zu analysieren. Z.B. EXPLAIN auf MySQL.


Einige Leute verwenden statt der von mir oben gezeigten Lösung Unterabfragen, aber ich finde meine Lösung erleichtert das Auflösen von Gleichständen.

3 Stimmen

Grundsätzlich günstig. Aber das hängt von der Marke der Datenbank ab, die Sie verwenden, sowie von der Menge und Verteilung der Daten in Ihrer Datenbank. Der einzige Weg, eine genaue Antwort zu erhalten, besteht darin, beide Lösungen mit Ihren Daten zu testen.

47 Stimmen

Wenn Sie Kunden einbeziehen möchten, die noch nie einen Kauf getätigt haben, ändern Sie JOIN purchase p1 ON (c.id = p1.customer_id) in LEFT JOIN purchase p1 ON (c.id = p1.customer_id).

0 Stimmen

Ich habe diese Lösung zusammen mit der Änderung von @GordonM bezüglich LEFT JOIN implementiert. Mein Problem jetzt ist, was ist, wenn ich 2 identische Zeilen habe. Gibt es eine Möglichkeit, dies auf die Rückgabe von nur 1 Zeile zu beschränken (es spielt keine Rolle welche)? Übrigens eine tolle Diskussion.

196voto

Adriaan Stander Punkte 155899

Sie könnten auch versuchen, dies mit einer Unterauswahl zu tun

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 Abfrage sollte alle Kunden und ihr letztes Kaufdatum verknüpfen.

6 Stimmen

@clu: Ändern Sie das INNER JOIN zu einem LEFT OUTER JOIN.

8 Stimmen

Sieht so aus, als ob davon ausgegangen wird, dass es an diesem Tag nur einen Kauf gibt. Wenn es zwei gäbe, würden Sie zwei Ausgabereihen für einen Kunden erhalten, oder?

0 Stimmen

Warum können wir nicht auf das letzte INNER JOIN verzichten?

58voto

Stefan Haberl Punkte 8595

Ein weiterer Ansatz wäre die Verwendung einer NOT EXISTS Bedingung in Ihrer Join-Bedingung, um später Kä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
   )
)

1 Stimmen

Können Sie den Teil AND NOT EXISTS in einfachen Worten erklären?

1 Stimmen

Der Unterselect überprüft nur, ob es eine Zeile mit einer höheren ID gibt. Sie erhalten nur eine Zeile in Ihrem Ergebnis, wenn keine mit höherer ID gefunden wird. Das sollte die eindeutig höchste sein.

2 Stimmen

Wenn die Id ein uniqueidentifier (guid) ist, kann dies nicht verwendet werden.

53voto

Tate Thurston Punkte 3482

Wenn Sie PostgreSQL verwenden, können Sie DISTINCT ON verwenden, um die erste Zeile in einer Gruppe zu finden.

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-Dokumentation - Distinct On

Beachten Sie, dass das DISTINCT ON-Feld bzw. die Felder - hier customer_id - mit den linksten Feldern in der ORDER BY-Klausel übereinstimmen müssen.

Warnung: Dies ist eine nicht standardmäßige Klausel.

0 Stimmen

Wie unterscheidet es sich von JOIN (SELECT * FROM purchase WHERE customer.customer_id = purchase.customer_id ORDER BY customer_id, date DESC LIMIT 1)

35voto

Madalina Dragomir Punkte 421

Sie haben die Datenbank nicht angegeben. Wenn es eine ist, die analytische Funktionen zulässt, kann es schneller sein, diesen Ansatz zu verwenden als den GROUP BY-Ansatz (auf jeden Fall schneller in Oracle, wahrscheinlich schneller in den neueren SQL Server-Editionen, weiß nicht über andere).

Die Syntax in SQL Server wäre:

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

13 Stimmen

Das ist die falsche Antwort auf die Frage, weil du "RANK()" anstatt von "ROW_NUMBER()" benutzt. RANK wird dir immer noch dasselbe Problem bei Unentschieden geben, wenn zwei Einkäufe das exakt gleiche Datum haben. Das ist die Funktion des Rankings; wenn die Top 2 übereinstimmen, bekommen sie beide den Wert 1 zugewiesen und der 3. Datensatz erhält den Wert 3. Mit Row_Number hingegen gibt es keine Unentschieden, es ist eindeutig für die gesamte Partition.

4 Stimmen

Versuche hier Bill Karwins Ansatz gegenüber dem von Madalina, mit aktivierten Ausführungsplänen unter SQL Server 2008. Ich habe festgestellt, dass Bill Karwins Ansatz eine Abfragekosten von 43% im Vergleich zu Madalinas Ansatz verwendet, der 57% betrug - trotz der eleganteren Syntax dieser Antwort würde ich immer noch die Version von Bill bevorzugen!

0 Stimmen

Ja, es muss stattdessen 'ROW_NUMBER()' geben. Gute Arbeit @Madalina

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X