MySQL Nur der letzten Zeile beitreten?

100

Ich habe einen Tabellenkunden, der eine Kunden-ID, eine E-Mail und eine Referenz speichert. Es gibt eine zusätzliche Tabelle customer_data, in der ein historischer Datensatz der am Kunden vorgenommenen Änderungen gespeichert ist. Wenn also eine Änderung vorgenommen wird, wird eine neue Zeile eingefügt.

Um die Kundeninformationen in einer Tabelle anzuzeigen, müssen die beiden Tabellen verknüpft werden. Es sollte jedoch nur die letzte Zeile aus customer_data mit der Kundentabelle verknüpft werden.

Es wird etwas komplizierter, da die Abfrage paginiert ist, also ein Limit und einen Offset hat.

Wie kann ich das mit MySQL machen? Ich glaube, ich möchte irgendwo einen DISTINCT einbauen ...

Die Abfrage im Moment ist wie folgt:

SELECT *, CONCAT(title,' ',forename,' ',surname) AS name
FROM customer c
INNER JOIN customer_data d on c.customer_id=d.customer_id
WHERE name LIKE '%Smith%' LIMIT 10, 20

Habe ich auch Recht, wenn ich denke, dass ich CONCAT mit LIKE auf diese Weise verwenden kann?

(Ich weiß zu schätzen, dass INNER JOIN möglicherweise die falsche Art von JOIN ist. Ich habe eigentlich keine Ahnung, was der Unterschied zwischen den verschiedenen JOINs ist. Ich werde das jetzt untersuchen!)

bcmcfc
quelle
Wie sieht die Kundenverlaufstabelle aus? Wie wird die letzte Zeile ermittelt? Gibt es ein Zeitstempelfeld?
Daniel Vassallo
Die letzte ist einfach die zuletzt eingefügte Zeile - der Primärschlüssel ist also die höchste Zahl.
bcmcfc
Warum nicht ein Auslöser? Schauen Sie sich diese Antwort an: stackoverflow.com/questions/26661314/…
Rodrigo Polo
Die meisten / alle Antworten dauerten mit Millionen von Zeilen zu lange. Es gibt einige Lösungen mit einer besseren Leistung.
Halil Özgür

Antworten:

142

Möglicherweise möchten Sie Folgendes ausprobieren:

SELECT    CONCAT(title, ' ', forename, ' ', surname) AS name
FROM      customer c
JOIN      (
              SELECT    MAX(id) max_id, customer_id 
              FROM      customer_data 
              GROUP BY  customer_id
          ) c_max ON (c_max.customer_id = c.customer_id)
JOIN      customer_data cd ON (cd.id = c_max.max_id)
WHERE     CONCAT(title, ' ', forename, ' ', surname) LIKE '%Smith%' 
LIMIT     10, 20;

Beachten Sie, dass a JOINnur ein Synonym für ist INNER JOIN.

Testfall:

CREATE TABLE customer (customer_id int);
CREATE TABLE customer_data (
   id int, 
   customer_id int, 
   title varchar(10),
   forename varchar(10),
   surname varchar(10)
);

INSERT INTO customer VALUES (1);
INSERT INTO customer VALUES (2);
INSERT INTO customer VALUES (3);

INSERT INTO customer_data VALUES (1, 1, 'Mr', 'Bobby', 'Smith');
INSERT INTO customer_data VALUES (2, 1, 'Mr', 'Bob', 'Smith');
INSERT INTO customer_data VALUES (3, 2, 'Mr', 'Jane', 'Green');
INSERT INTO customer_data VALUES (4, 2, 'Miss', 'Jane', 'Green');
INSERT INTO customer_data VALUES (5, 3, 'Dr', 'Jack', 'Black');

Ergebnis (Abfrage ohne LIMITund WHERE):

SELECT    CONCAT(title, ' ', forename, ' ', surname) AS name
FROM      customer c
JOIN      (
              SELECT    MAX(id) max_id, customer_id 
              FROM      customer_data 
              GROUP BY  customer_id
          ) c_max ON (c_max.customer_id = c.customer_id)
JOIN      customer_data cd ON (cd.id = c_max.max_id);

+-----------------+
| name            |
+-----------------+
| Mr Bob Smith    |
| Miss Jane Green |
| Dr Jack Black   |
+-----------------+
3 rows in set (0.00 sec)
Daniel Vassallo
quelle
2
Vielen Dank für die Detailgenauigkeit, auf die Sie dort eingegangen sind. Ich hoffe, es hilft anderen und nur mir!
bcmcfc
20
Auf lange Sicht kann dieser Ansatz zu Leistungsproblemen führen, da eine temporäre Tabelle erstellt werden muss. Eine andere Lösung (wenn möglich) besteht darin, ein neues boolesches Feld (is_last) in customer_data hinzuzufügen, das Sie jedes Mal aktualisieren müssten, wenn ein neuer Eintrag hinzugefügt wird. Der letzte Eintrag hat is_last = 1, alle anderen für diesen Kunden - is_last = 0.
Cephuo
4
Die Leute sollten (bitte) auch die folgende Antwort lesen (von Danny Coulombe), da diese Antwort (sorry Daniel) mit längeren Abfragen / mehr Daten furchtbar langsam ist. Ich habe meine Seite 12 Sekunden lang "warten" lassen, um sie zu laden. Überprüfen Sie daher auch stackoverflow.com/a/35965649/2776747 . Ich habe es erst nach vielen anderen Änderungen bemerkt, also habe ich sehr lange gebraucht, um es herauszufinden.
Art
Sie haben keine Ahnung, wie sehr mir das geholfen hat :) Danke master
node_man
102

Wenn Sie mit umfangreichen Abfragen arbeiten, verschieben Sie die Anforderung für die neueste Zeile besser in die where-Klausel. Es ist viel schneller und sieht sauberer aus.

SELECT c.*,
FROM client AS c
LEFT JOIN client_calling_history AS cch ON cch.client_id = c.client_id
WHERE
   cch.cchid = (
      SELECT MAX(cchid)
      FROM client_calling_history
      WHERE client_id = c.client_id AND cal_event_id = c.cal_event_id
   )
Danny Coulombe
quelle
4
Wow, ich bin fast ungläubig darüber, wie groß der Leistungsunterschied ist. Ich bin mir nicht sicher, warum das noch so drastisch war, aber bisher war es so viel schneller, dass es sich anfühlt, als hätte ich es woanders versaut ...
Brian Leishman
2
Ich wünschte wirklich, ich könnte dies mehr als einmal +1, damit es mehr gesehen wird. Ich habe dies ziemlich oft getestet und irgendwie werden meine Abfragen dadurch praktisch augenblicklich (WorkBench sagt buchstäblich 0,000 Sekunden, auch mit sql_no_cache set), während die Suche im Join mehrere Sekunden dauerte. Immer noch verblüfft, aber ich meine, mit solchen Ergebnissen kann man nicht streiten.
Brian Leishman
1
Sie verbinden zuerst direkt 2 Tabellen und filtern dann mit WHERE. Ich denke, es ist ein massives Leistungsproblem, wenn Sie eine Million Kunden und zig Millionen Anrufe haben. Weil SQL zuerst versucht, zwei Tabellen zu verbinden und dann auf den einzelnen Client herunterzufiltern. Ich würde lieber die Clients und die zugehörigen Aufrufhistorien zuerst in einer Unterabfrage aus den Tabellen filtern und dann die Tabellen verbinden.
Tarik
1
Ich nehme an, dass "ca.client_id" und "ca.cal_event_id" für beide "c" sein müssen.
Herbert Van-Vliet
1
Ich stimme @NickCoons zu. NULL-Werte werden nicht zurückgegeben, da sie von der where-Klausel ausgeschlossen werden. Wie würden Sie vorgehen, um die NULL-Werte einzuschließen und dennoch die hervorragende Leistung dieser Abfrage beizubehalten?
aanders77
10

Unter der Annahme, dass die Autoincrement-Spalte in customer_databenannt ist Id, können Sie Folgendes tun:

SELECT CONCAT(title,' ',forename,' ',surname) AS name *
FROM customer c
    INNER JOIN customer_data d 
        ON c.customer_id=d.customer_id
WHERE name LIKE '%Smith%'
    AND d.ID = (
                Select Max(D2.Id)
                From customer_data As D2
                Where D2.customer_id = D.customer_id
                )
LIMIT 10, 20
Thomas
quelle
9

Für alle, die mit einer älteren Version von MySQL (vor 5.0 ish) arbeiten müssen, können Sie keine Unterabfragen für diesen Abfragetyp durchführen. Hier ist die Lösung, die ich machen konnte und die anscheinend großartig funktionierte.

SELECT MAX(d.id), d2.*, CONCAT(title,' ',forename,' ',surname) AS name
FROM customer AS c 
LEFT JOIN customer_data as d ON c.customer_id=d.customer_id 
LEFT JOIN customer_data as d2 ON d.id=d2.id
WHERE CONCAT(title, ' ', forename, ' ', surname) LIKE '%Smith%'
GROUP BY c.customer_id LIMIT 10, 20;

Im Wesentlichen wird dabei die maximale ID Ihrer Datentabelle ermittelt, die mit dem Kunden verknüpft ist, und anschließend die Datentabelle mit der gefundenen maximalen ID verknüpft. Der Grund dafür ist, dass die Auswahl des Maximums einer Gruppe nicht garantiert, dass der Rest der Daten mit der ID übereinstimmt, es sei denn, Sie verbinden sie wieder mit sich selbst.

Ich habe dies nicht auf neueren Versionen von MySQL getestet, aber es funktioniert unter 4.0.30.

payne8
quelle
Dies ist exquisit in seiner Einfachheit. Warum habe ich diesen Ansatz zum ersten Mal gesehen? Beachten Sie, EXPLAINdass dies eine temporäre Tabelle und einen Dateisort verwendet. Durch Hinzufügen ORDER BY NULLam Ende wird der Dateisort entfernt.
Timo
Zu meinem Bedauern ist meine eigene, nicht so schöne Lösung für meine Daten 3,5-mal so schnell. Ich habe eine Unterabfrage verwendet, um die Haupttabelle sowie die neuesten IDs der verknüpften Tabellen auszuwählen, und dann eine äußere Abfrage, die die Unterabfrage auswählt und die tatsächlichen Daten aus den verknüpften Tabellen liest. Ich verbinde 5 Tabellen mit der Haupttabelle und teste mit einer where-Bedingung, die 1000 Datensätze auswählt. Indizes sind optimal.
Timo
Ich habe Ihre Lösung mit verwendet SELECT *, MAX(firstData.id), MAX(secondData.id) [...]. Durch den Wechsel zu konnte SELECT main.*, firstData2.*, secondData2.*, MAX(firstData.id), MAX(secondData.id), [...]ich es logischerweise deutlich schneller machen. Auf diese Weise können die ersten Joins nur aus dem Index lesen, anstatt auch alle Daten aus dem Primärindex lesen zu müssen. Jetzt dauert die hübsche Lösung nur noch 1,9-mal so lange wie die auf Unterabfragen basierende Lösung.
Timo
In MySQL 5.7 funktioniert es nicht mehr. Jetzt gibt d2. * Daten für die erste Zeile in der Gruppe zurück, nicht für die letzte. SELECT MAX (R1.id), R2. * FROM Rechnungen I LEFT JOIN-Antworten R1 ON I.id = R1.invoice_id LEFT JOIN-Antworten R2 ON R1.id = R2.id GROUP BY I.id LIMIT 0,10
Marco Marsala
5

Ich weiß, dass diese Frage alt ist, aber sie hat im Laufe der Jahre viel Aufmerksamkeit erhalten, und ich denke, es fehlt ein Konzept, das jemandem in einem ähnlichen Fall helfen könnte. Der Vollständigkeit halber füge ich es hier hinzu.

Wenn Sie Ihr ursprüngliches Datenbankschema nicht ändern können, wurden viele gute Antworten gegeben, um das Problem einwandfrei zu lösen.

Wenn Sie können jedoch Ihr Schema ändern, würde ich raten , ein Feld in Ihrem hinzuzufügen customerTabelle , die das hält idder neuesten customer_dataRekord für diesen Kunden:

CREATE TABLE customer (
  id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  current_data_id INT UNSIGNED NULL DEFAULT NULL
);

CREATE TABLE customer_data (
   id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
   customer_id INT UNSIGNED NOT NULL, 
   title VARCHAR(10) NOT NULL,
   forename VARCHAR(10) NOT NULL,
   surname VARCHAR(10) NOT NULL
);

Kunden abfragen

Das Abfragen ist so einfach und schnell wie möglich:

SELECT c.*, d.title, d.forename, d.surname
FROM customer c
INNER JOIN customer_data d on d.id = c.current_data_id
WHERE ...;

Der Nachteil ist die zusätzliche Komplexität beim Erstellen oder Aktualisieren eines Kunden.

Kunden aktualisieren

Wenn Sie einen Kunden aktualisieren möchten, fügen Sie einen neuen Datensatz in die customer_dataTabelle ein und aktualisieren den customerDatensatz.

INSERT INTO customer_data (customer_id, title, forename, surname) VALUES(2, 'Mr', 'John', 'Smith');
UPDATE customer SET current_data_id = LAST_INSERT_ID() WHERE id = 2;

Kunden erstellen

Beim Erstellen eines Kunden müssen Sie lediglich den customerEintrag einfügen und dann dieselben Anweisungen ausführen:

INSERT INTO customer () VALUES ();

SET @customer_id = LAST_INSERT_ID();
INSERT INTO customer_data (customer_id, title, forename, surname) VALUES(@customer_id, 'Mr', 'John', 'Smith');
UPDATE customer SET current_data_id = LAST_INSERT_ID() WHERE id = @customer_id;

Einpacken

Die zusätzliche Komplexität beim Erstellen / Aktualisieren eines Kunden mag furchterregend sein, kann jedoch leicht mit Triggern automatisiert werden.

Wenn Sie ein ORM verwenden, kann dies sehr einfach zu verwalten sein. Das ORM kann sich darum kümmern, die Werte einzufügen, die IDs zu aktualisieren und die beiden Tabellen automatisch für Sie zu verbinden.

So Customerwürde Ihr veränderliches Modell aussehen:

class Customer
{
    private int id;
    private CustomerData currentData;

    public Customer(String title, String forename, String surname)
    {
        this.update(title, forename, surname);
    }

    public void update(String title, String forename, String surname)
    {
        this.currentData = new CustomerData(this, title, forename, surname);
    }

    public String getTitle()
    {
        return this.currentData.getTitle();
    }

    public String getForename()
    {
        return this.currentData.getForename();
    }

    public String getSurname()
    {
        return this.currentData.getSurname();
    }
}

Und Ihr unveränderliches CustomerDataModell, das nur Getter enthält:

class CustomerData
{
    private int id;
    private Customer customer;
    private String title;
    private String forename;
    private String surname;

    public CustomerData(Customer customer, String title, String forename, String surname)
    {
        this.customer = customer;
        this.title    = title;
        this.forename = forename;
        this.surname  = surname;
    }

    public String getTitle()
    {
        return this.title;
    }

    public String getForename()
    {
        return this.forename;
    }

    public String getSurname()
    {
        return this.surname;
    }
}
Benjamin
quelle
Ich habe diesen Ansatz mit der Lösung von @ payne8 (oben) kombiniert, um das gewünschte Ergebnis ohne Unterabfragen zu erzielen.
Ingwer und Lavendel
2
SELECT CONCAT(title,' ',forename,' ',surname) AS name * FROM customer c 
INNER JOIN customer_data d on c.id=d.customer_id WHERE name LIKE '%Smith%' 

Ich denke, Sie müssen c.customer_id in c.id ändern

sonst Tabellenstruktur aktualisieren

Pramendra Gupta
quelle
Ich habe abgelehnt, weil ich Ihre Antwort falsch verstanden habe und anfangs dachte, es sei falsch.
Eile
1

Sie können dies auch tun

SELECT    CONCAT(title, ' ', forename, ' ', surname) AS name
FROM      customer c
LEFT JOIN  (
              SELECT * FROM  customer_data ORDER BY id DESC
          ) customer_data ON (customer_data.customer_id = c.customer_id)
GROUP BY  c.customer_id          
WHERE     CONCAT(title, ' ', forename, ' ', surname) LIKE '%Smith%' 
LIMIT     10, 20;
Ajay Kumar
quelle
0

Es ist eine gute Idee, die tatsächlichen Daten in der Tabelle " customer_data " zu protokollieren . Mit diesen Daten können Sie alle Daten aus der Tabelle "customer_data" auswählen, wie Sie möchten.

Burçin
quelle