MySQL JOIN uniquement sur la ligne la plus récente?

103

J'ai une table client qui stocke un customer_id, un e-mail et une référence. Il existe une table supplémentaire customer_data qui stocke un enregistrement historique des modifications apportées au client, c'est-à-dire qu'une nouvelle ligne est insérée en cas de modification.

Afin d'afficher les informations client dans une table, les deux tables doivent être jointes, mais seule la ligne la plus récente de customer_data doit être jointe à la table customer.

Cela devient un peu plus compliqué dans la mesure où la requête est paginée, donc a une limite et un décalage.

Comment puis-je faire cela avec MySQL? Je pense que je veux mettre un DISTINCT là-dedans quelque part ...

La requête à la minute est comme ça-

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

De plus, ai-je raison de penser que je peux utiliser CONCAT avec LIKE de cette manière?

(J'apprécie qu'INNER JOIN puisse être le mauvais type de JOIN à utiliser. Je n'ai en fait aucune idée de la différence entre les différents JOIN. Je vais examiner cela maintenant!)

bcmcfc
la source
À quoi ressemble le tableau de l'historique des clients? Comment la ligne la plus récente est-elle déterminée? Existe-t-il un champ d'horodatage?
Daniel Vassallo
Le plus récent est simplement la dernière ligne insérée - sa clé primaire est donc le nombre le plus élevé.
bcmcfc
Pourquoi pas un déclencheur? jetez un oeil à cette réponse: stackoverflow.com/questions/26661314/…
Rodrigo Polo
La plupart / toutes les réponses prenaient trop de temps avec des millions de lignes. Il existe des solutions avec de meilleures performances.
Halil Özgür

Réponses:

142

Vous pouvez essayer ce qui suit:

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;

Notez que a JOINn'est qu'un synonyme de INNER JOIN.

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

Résultat (requête sans LIMITet 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
la source
2
Merci pour le niveau de détail dans lequel vous êtes allé. J'espère que cela aide les autres ainsi que moi!
bcmcfc
21
À long terme, cette approche pourrait créer des problèmes de performances car elle aurait besoin de créer une table temporaire. Une autre solution (si possible) est donc d'ajouter un nouveau champ booléen (is_last) dans customer_data que vous devrez mettre à jour à chaque fois qu'une nouvelle entrée est ajoutée. La dernière entrée aura is_last = 1, toutes les autres pour ce client - is_last = 0.
cephuo
5
Les gens devraient (s'il vous plaît) lire également la réponse suivante (de Danny Coulombe), car cette réponse (désolé Daniel) est terriblement lente avec des requêtes plus longues / plus de données. J'ai fait «attendre» ma page pendant 12 secondes pour charger; Veuillez donc également consulter stackoverflow.com/a/35965649/2776747 . Je ne l'ai remarqué qu'après de nombreux autres changements, il m'a donc fallu beaucoup de temps pour le découvrir.
Art
Vous n'avez aucune idée à quel point cela m'a aidé :) Merci maître
node_man
104

Si vous travaillez avec des requêtes lourdes, il vaut mieux déplacer la demande de la dernière ligne dans la clause where. C'est beaucoup plus rapide et semble plus propre.

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
la source
4
Wow, je suis presque incrédule à quel point c'est une différence de performance. Je ne sais pas pourquoi c'était encore si radical, mais jusqu'à présent, c'était tellement plus rapide que j'ai l'impression d'avoir foiré ailleurs ...
Brian Leishman
2
J'aimerais vraiment pouvoir attribuer +1 à cela plus d'une fois pour qu'il soit plus visible. J'ai testé cela un peu et cela rend mes requêtes pratiquement instantanées (WorkBench dit littéralement 0,000 secondes, même avec sql_no_cache set), alors que la recherche dans la jointure a pris plusieurs secondes. Toujours dérouté, mais je veux dire que vous ne pouvez pas discuter avec des résultats comme celui-là.
Brian Leishman
1
Vous joignez d'abord directement 2 tables, puis filtrez avec WHERE. Je pense que c'est un problème de performances énorme si vous avez un million de clients et des dizaines de millions d'historique d'appels. Parce que SQL essaiera d'abord de joindre 2 tables, puis de filtrer vers le client unique. Je préfère filtrer les clients et les historiques d'appels associés à partir des tables d'abord dans une sous-requête, puis joindre les tables.
Tarik
1
Je suppose que "ca.client_id" et "ca.cal_event_id" doivent être "c" pour les deux.
Herbert Van-Vliet
1
Je suis d'accord avec @NickCoons. Les valeurs NULL ne seront pas renvoyées car elles sont exclues par la clause where. Comment feriez-vous pour inclure les valeurs NULL tout en conservant les excellentes performances de cette requête?
aanders77
10

En supposant que la colonne d'auto-incrémentation customer_dataest nommée Id, vous pouvez faire:

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
la source
9

Pour toute personne qui doit travailler avec une ancienne version de MySQL (pré-5.0 ish), vous ne pouvez pas faire de sous-requêtes pour ce type de requête. Voici la solution que j'ai pu faire et cela a semblé très bien fonctionner.

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;

Il s'agit essentiellement de trouver l'ID maximum de votre table de données en le joignant au client, puis de joindre la table de données à l'ID maximum trouvé. La raison en est que la sélection du maximum d'un groupe ne garantit pas que le reste des données correspond à l'ID, sauf si vous le rejoignez sur lui-même.

Je n'ai pas testé cela sur les versions plus récentes de MySQL mais cela fonctionne sur 4.0.30.

payne8
la source
C'est exquis dans sa simplicité. Pourquoi est-ce la première fois que je vois cette approche? Notez que cela EXPLAINindique que cela utilise une table temporaire et un tri de fichiers. L'ajout ORDER BY NULLà la fin supprime le tri de fichiers.
Timo
À mon regret, ma propre solution pas aussi belle est 3,5 fois plus rapide pour mes données. J'ai utilisé une sous-requête pour sélectionner la table principale plus les identifiants les plus récents des tables jointes, puis une requête externe qui sélectionne la sous-requête et lit les données réelles des tables jointes. Je joins 5 tables sur la table principale et teste avec une condition where qui sélectionne 1000 enregistrements. Les index sont optimaux.
Timo
J'utilisais votre solution avec SELECT *, MAX(firstData.id), MAX(secondData.id) [...]. Logiquement, en passant à, SELECT main.*, firstData2.*, secondData2.*, MAX(firstData.id), MAX(secondData.id), [...]j'ai pu le rendre beaucoup plus rapide. Cela permet aux premières jointures de lire uniquement à partir de l'index, plutôt que d'avoir à lire toutes les données de l'index primaire. Maintenant, la jolie solution ne prend que 1,9 fois plus de temps que la solution basée sur les sous-requêtes.
Timo
Cela ne fonctionne plus dans MySQL 5.7. Désormais, d2. * Renverra les données de la première ligne du groupe, pas de la dernière. SELECT MAX (R1.id), R2. * FROM factures I LEFT JOIN réponses R1 ON I.id = R1.invoice_id LEFT JOIN réponses R2 ON R1.id = R2.id GROUP BY I.id LIMIT 0,10
Marco Marsala
5

Je sais que cette question est ancienne, mais elle a retenu beaucoup d'attention au fil des ans et je pense qu'il manque un concept qui pourrait aider quelqu'un dans un cas similaire. Je l'ajoute ici par souci d'exhaustivité.

Si vous ne pouvez pas modifier votre schéma de base de données d'origine, de nombreuses bonnes réponses ont été fournies et résolvent très bien le problème.

Si vous pouvez cependant modifier votre schéma, je vous conseillerais d'ajouter un champ dans votre customertable qui contient idle dernier customer_dataenregistrement de ce client:

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

Interroger les clients

L'interrogation est aussi simple et rapide que possible:

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

L'inconvénient est la complexité supplémentaire lors de la création ou de la mise à jour d'un client.

Mettre à jour un client

Chaque fois que vous souhaitez mettre à jour un client, vous insérez un nouvel enregistrement dans la customer_datatable et mettez à jour l' customerenregistrement.

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;

Créer un client

Créer un client consiste simplement à insérer l' customerentrée, puis à exécuter les mêmes instructions:

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;

Emballer

La complexité supplémentaire pour créer / mettre à jour un client peut être redoutable, mais elle peut facilement être automatisée avec des déclencheurs.

Enfin, si vous utilisez un ORM, cela peut être très facile à gérer. L'ORM peut se charger d'insérer les valeurs, de mettre à jour les identifiants et de joindre automatiquement les deux tables pour vous.

Voici à quoi Customerressemblerait votre modèle mutable :

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

Et votre CustomerDatamodèle immuable , qui ne contient que des getters:

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
la source
J'ai combiné cette approche avec la solution de @ payne8 (ci-dessus) pour obtenir le résultat souhaité sans aucune sous-requête.
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%' 

je pense que vous devez changer c.customer_id en c.id

else mettre à jour la structure de la table

Pramendra Gupta
la source
J'ai voté contre parce que j'ai mal lu votre réponse et j'ai d'abord pensé que c'était faux. La hâte est un mauvais conseiller :-)
Wirone
1

Vous pouvez également le faire

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
la source
0

C'est une bonne idée de consigner les données réelles dans la table " customer_data ". Avec ces données, vous pouvez sélectionner toutes les données de la table "customer_data" comme vous le souhaitez.

Burçin
la source