MySQL PARTECIPA solo alla riga più recente?


103

Ho un cliente della tabella che memorizza un customer_id, un'e-mail e un riferimento. È presente una tabella aggiuntiva customer_data che memorizza un record storico delle modifiche apportate al cliente, ovvero quando c'è una modifica apportata viene inserita una nuova riga.

Per visualizzare le informazioni sul cliente in una tabella, le due tabelle devono essere unite, tuttavia solo la riga più recente di customer_data deve essere unita alla tabella del cliente.

Diventa un po 'più complicato in quanto la query è impaginata, quindi ha un limite e un offset.

Come posso farlo con MySQL? Penso di voler inserire un DISTINCT da qualche parte ...

La domanda al momento è così-

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

Inoltre, ho ragione nel pensare di poter usare CONCAT con LIKE in questo modo?

(Mi rendo conto che INNER JOIN potrebbe essere il tipo sbagliato di JOIN da usare. In realtà non ho idea di quale sia la differenza tra i diversi JOIN. Lo esaminerò ora!)


Come appare la tabella della cronologia dei clienti? Come viene determinata la riga più recente? C'è un campo timestamp?
Daniel Vassallo

Il più recente è semplicemente l'ultima riga inserita, quindi la sua chiave primaria è il numero più alto.
bcmcfc

Perché non un trigger? dai un'occhiata a questa risposta: stackoverflow.com/questions/26661314/…
Rodrigo Polo

La maggior parte / tutte le risposte stavano impiegando troppo tempo con milioni di righe. Ci sono alcune soluzioni con prestazioni migliori.
Halil Özgür

Risposte:


142

Potresti provare quanto segue:

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;

Nota che a JOINè solo un sinonimo di INNER JOIN.

Scenario di test:

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');

Risultato (query senza LIMITe 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)

2
Grazie per il livello di dettaglio in cui sei entrato. Spero che aiuti gli altri così come me stesso!
bcmcfc

21
A lungo termine questo approccio potrebbe creare problemi di prestazioni poiché richiederebbe la creazione di una tabella temporanea. Quindi un'altra soluzione (se possibile) è aggiungere un nuovo campo booleano (is_last) in customer_data che dovresti aggiornare ogni volta che viene aggiunta una nuova voce. L'ultima voce avrà is_last = 1, tutte le altre per questo cliente - is_last = 0.
cephuo

5
Le persone dovrebbero (per favore) leggere anche la seguente risposta (di Danny Coulombe), perché questa risposta (scusa Daniel) è terribilmente lenta con query più lunghe / più dati. Ho fatto "aspettare" la mia pagina per 12 secondi per caricarsi; Quindi controlla anche stackoverflow.com/a/35965649/2776747 . Non l'ho notato fino a dopo molti altri cambiamenti, quindi mi ci è voluto molto tempo per scoprirlo.
Art

Non hai idea di quanto questo mi abbia aiutato :) Grazie maestro
node_man

104

Se stai lavorando con query pesanti, è meglio spostare la richiesta per l'ultima riga nella clausola where. È molto più veloce e sembra più pulito.

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
   )

4
Wow, sono quasi incredulo su quanta differenza di prestazioni sia questa. Non sono sicuro del motivo per cui sia stato ancora così drastico, ma finora è stato molto più veloce che mi sembra di aver sbagliato da qualche altra parte ...
Brian Leishman

2
Vorrei davvero poter fare +1 su questo più di una volta in modo che venga visto di più. L'ho testato un po 'e in qualche modo rende le mie query praticamente istantanee (WorkBench dice letteralmente 0.000 secondi, anche con sql_no_cache set), mentre per completare la ricerca nel join sono voluti più secondi. Ancora sconcertato, ma voglio dire che non puoi discutere con risultati del genere.
Brian Leishman

1
Stai unendo direttamente 2 tabelle prima e poi stai filtrando con WHERE. Penso che sia un enorme problema di prestazioni se si dispone di un milione di client e decine di milioni di cronologia delle chiamate. Perché SQL proverà prima a unire 2 tabelle e quindi filtrare fino al singolo client. Preferirei filtrare i client e le cronologie delle chiamate correlate dalle tabelle prima in una sottoquery e poi unirmi alle tabelle.
Tarik

1
Suppongo che "ca.client_id" e "ca.cal_event_id" debbano essere "c" per entrambi.
Herbert Van-Vliet

1
Sono d'accordo con @NickCoons. I valori NULL non verranno restituiti perché sono esclusi dalla clausola where. Come faresti per includere i valori NULL e mantenere le prestazioni eccellenti di questa query?
aanders77

10

Supponendo che la colonna di incremento automatico in customer_dataè denominata Id, puoi fare:

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

9

Chiunque debba lavorare con una versione precedente di MySQL (pre-5.0 ish) non è in grado di eseguire sottoquery per questo tipo di query. Ecco la soluzione che sono riuscito a fare e sembrava funzionare alla grande.

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;

Essenzialmente si tratta di trovare l'id massimo della tabella dati unendolo al cliente e quindi unendo la tabella dati all'id massimo trovato. La ragione di ciò è che la selezione del massimo di un gruppo non garantisce che il resto dei dati corrisponda all'ID a meno che non ci si unisca di nuovo su se stesso.

Non l'ho testato su versioni più recenti di MySQL ma funziona su 4.0.30.


Questo è squisito nella sua semplicità. Perché è la prima volta che vedo questo approccio? Notare che EXPLAINindica che questo utilizza una tabella e un filesort temporanei. L'aggiunta ORDER BY NULLalla fine elimina il filesort.
Timo

Con mio dispiacere, la mia soluzione non così bella è 3,5 volte più veloce per i miei dati. Ho utilizzato una sottoquery per selezionare la tabella principale più gli ID più recenti delle tabelle unite, quindi una query esterna che seleziona la sottoquery e legge i dati effettivi dalle tabelle unite. Sto unendo 5 tabelle sulla tabella principale e sto testando con una condizione where che seleziona 1000 record. Gli indici sono ottimali.
Timo

Stavo usando la tua soluzione con SELECT *, MAX(firstData.id), MAX(secondData.id) [...]. Logicamente, passando a SELECT main.*, firstData2.*, secondData2.*, MAX(firstData.id), MAX(secondData.id), [...]sono stato in grado di renderlo molto più veloce. Ciò consente ai primi join di leggere solo dall'indice, invece di dover leggere anche tutti i dati dall'indice primario. Ora la bella soluzione richiede solo 1,9 volte il tempo della soluzione basata su subquery.
Timo

Non funziona più in MySQL 5.7. Ora d2. * Restituirà i dati per la prima riga del gruppo, non per l'ultima. SELEZIONA MAX (R1.id), R2. * FROM fatture I LEFT JOIN responses R1 ON I.id = R1.invoice_id LEFT JOIN responses R2 ON R1.id = R2.id GROUP BY I.id LIMIT 0,10
Marco Marsala

5

So che questa domanda è vecchia, ma ha ricevuto molta attenzione nel corso degli anni e penso che manchi un concetto che possa aiutare qualcuno in un caso simile. Lo aggiungo qui per completezza.

Se non è possibile modificare lo schema del database originale, sono state fornite molte buone risposte e il problema è stato risolto perfettamente.

Se puoi , tuttavia, modificare il tuo schema, ti consiglio di aggiungere un campo nella tua customertabella che contiene idl'ultimo customer_datarecord per questo cliente:

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
);

Interrogare i clienti

La query è il più facile e veloce possibile:

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

Lo svantaggio è la complessità aggiuntiva durante la creazione o l'aggiornamento di un cliente.

Aggiornamento di un cliente

Ogni volta che desideri aggiornare un cliente, inserisci un nuovo record nella customer_datatabella e aggiorna il customerrecord.

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;

Creare un cliente

Creare un cliente è solo questione di inserire la customervoce, quindi eseguire le stesse istruzioni:

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;

Avvolgendo

La complessità aggiuntiva per la creazione / aggiornamento di un cliente potrebbe essere spaventosa, ma può essere facilmente automatizzata con trigger.

Infine, se stai usando un ORM, questo può essere davvero facile da gestire. L'ORM può occuparsi di inserire i valori, aggiornare gli ID e unire automaticamente le due tabelle.

Ecco come Customerapparirebbe il tuo modello mutevole :

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();
    }
}

E il tuo CustomerDatamodello immutabile , che contiene solo getter:

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;
    }
}

Ho combinato questo approccio con la soluzione di @ payne8 (sopra) per ottenere il risultato desiderato senza sottoquery.
Ginger and Lavender

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%' 

penso che tu debba cambiare c.customer_id in c.id

altrimenti aggiorna la struttura della tabella


Ho svalutato perché ho letto male la tua risposta e inizialmente ho pensato che fosse sbagliata. La fretta è una cattiva consigliera :-)
Wirone

1

Puoi anche farlo

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;

0

È una buona idea quella di registrare i dati effettivi nella tabella " customer_data ". Con questi dati puoi selezionare tutti i dati dalla tabella "customer_data" come desideri.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.