MySQL JOIN solo en la fila más reciente?

103

Tengo un cliente de mesa que almacena un customer_id, un correo electrónico y una referencia. Hay una tabla adicional customer_data que almacena un registro histórico de los cambios realizados al cliente, es decir, cuando se realiza un cambio, se inserta una nueva fila.

Para mostrar la información del cliente en una tabla, las dos tablas deben unirse; sin embargo, solo la fila más reciente de customer_data debe unirse a la tabla de clientes.

Se vuelve un poco más complicado porque la consulta está paginada, por lo que tiene un límite y un desplazamiento.

¿Cómo puedo hacer esto con MySQL? Creo que quiero poner un DISTINCT en alguna parte ...

La consulta al minuto es así-

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

Además, ¿tengo razón al pensar que puedo usar CONCAT con LIKE de esta manera?

(Aprecio que INNER JOIN podría ser el tipo incorrecto de JOIN para usar. De hecho, no tengo ni idea de cuál es la diferencia entre los diferentes JOIN. ¡Voy a investigar eso ahora!)

bcmcfc
fuente
¿Cómo se ve la tabla del historial del cliente? ¿Cómo se determina la fila más reciente? ¿Hay un campo de marca de tiempo?
Daniel Vassallo
La más reciente es simplemente la última fila insertada, por lo que su clave principal es el número más alto.
bcmcfc
¿Por qué no un disparador? eche un vistazo a esta respuesta: stackoverflow.com/questions/26661314/…
Rodrigo Polo
La mayoría / todas las respuestas estaban tardando demasiado con millones de filas. Existen algunas soluciones con un mejor rendimiento.
Halil Özgür

Respuestas:

142

Es posible que desee probar lo siguiente:

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;

Tenga en cuenta que a JOINes solo un sinónimo de INNER JOIN.

Caso de prueba:

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

Resultado (consulta sin LIMITy 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
fuente
2
Gracias por el nivel de detalle que ha analizado. ¡Espero que ayude a otros además de a mí!
bcmcfc
21
A largo plazo, este enfoque podría crear problemas de rendimiento, ya que sería necesario crear una tabla temporal. Entonces, otra solución (si es posible) es agregar un nuevo campo booleano (is_last) en customer_data que tendría que actualizar cada vez que se agregue una nueva entrada. La última entrada tendrá is_last = 1, todas las demás para este cliente - is_last = 0.
cephuo
5
La gente debería (por favor) leer también la siguiente respuesta (de Danny Coulombe), porque esta respuesta (lo siento Daniel) es terriblemente lenta con consultas más largas / más datos. Hice que mi página "esperara" 12 segundos para cargarse; Por lo tanto, consulte también stackoverflow.com/a/35965649/2776747 . No lo noté hasta después de muchos otros cambios, así que me tomó mucho tiempo descubrirlo.
Art
No tienes idea de cuánto me ha ayudado esto :) Gracias maestro
node_man
104

Si está trabajando con consultas pesadas, es mejor que mueva la solicitud a la última fila en la cláusula where. Es mucho más rápido y se ve más limpio.

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
fuente
4
Wow, casi no me puedo creer la diferencia de rendimiento que esto representa. No estoy seguro de por qué fue tan drástico todavía, pero hasta ahora fue mucho más rápido que parece que me equivoqué en otro lugar ...
Brian Leishman
2
Realmente desearía poder hacer +1 en esto más de una vez para que se vea más. He probado esto bastante y de alguna manera hace que mis consultas sean virtualmente instantáneas (WorkBench literalmente dice 0.000 segundos, incluso con sql_no_cache set), mientras que hacer la búsqueda en la combinación tardó varios segundos en completarse. Todavía desconcertado, pero quiero decir que no puedes discutir con resultados como ese.
Brian Leishman
1
Primero está uniendo directamente 2 tablas y luego filtrando con WHERE. Creo que es un problema de rendimiento masivo si tienes un millón de clientes y decenas de millones de historial de llamadas. Porque SQL intentará unir 2 tablas primero y luego filtrará al cliente único. Preferiría filtrar los clientes y los historiales de llamadas relacionados de las tablas primero en una subconsulta y luego unir las tablas.
Tarik
1
Supongo que "ca.client_id" y "ca.cal_event_id" deben ser "c" para ambos.
Herbert Van-Vliet
1
Estoy de acuerdo con @NickCoons. Los valores NULL no se devolverán porque están excluidos por la cláusula where. ¿Cómo haría para incluir los valores NULL y aún así mantener el excelente rendimiento de esta consulta?
aanders77
10

Suponiendo que la columna de incremento automático en customer_datase llama Id, que puede hacer:

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
fuente
9

Para cualquier persona que deba trabajar con una versión anterior de MySQL (anterior a 5.0 ish), no puede realizar subconsultas para este tipo de consulta. Aquí está la solución que pude hacer y pareció funcionar muy bien.

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;

Básicamente, se trata de encontrar la identificación máxima de su tabla de datos uniéndola al cliente y luego uniendo la tabla de datos a la identificación máxima encontrada. La razón de esto es porque seleccionar el máximo de un grupo no garantiza que el resto de los datos coincida con la identificación a menos que lo vuelva a unir a sí mismo.

No lo he probado en versiones más recientes de MySQL pero funciona en 4.0.30.

payne8
fuente
Esto es exquisito en su simplicidad. ¿Por qué es la primera vez que veo este enfoque? Tenga en cuenta que EXPLAINindica que esto usa una tabla temporal y un ordenamiento de archivos. Agregar ORDER BY NULLal final elimina el ordenamiento de archivos.
Timo
A mi pesar, mi propia solución no tan hermosa es 3,5 veces más rápida para mis datos. Usé una subconsulta para seleccionar la tabla principal más los ID más recientes de las tablas unidas, y luego una consulta externa que selecciona la subconsulta y lee los datos reales de las tablas unidas. Estoy uniendo 5 tablas a la tabla principal y probando con una condición where que selecciona 1000 registros. Los índices son óptimos.
Timo
Estaba usando tu solución con SELECT *, MAX(firstData.id), MAX(secondData.id) [...]. Lógicamente, al cambiar a SELECT main.*, firstData2.*, secondData2.*, MAX(firstData.id), MAX(secondData.id), [...]pude hacerlo significativamente más rápido. Esto permite que las primeras uniones lean solo del índice, en lugar de tener que leer también todos los datos del índice principal. Ahora, la solución bonita tarda solo 1,9 veces más que la solución basada en subconsultas.
Timo
Ya no funciona en MySQL 5.7. Ahora d2. * Devolverá datos para la primera fila del grupo, no la última. SELECT MAX (R1.id), R2. * FROM facturas I LEFT JOIN responde R1 ON I.id = R1.invoice_id LEFT JOIN responde R2 ON R1.id = R2.id GROUP BY I.id LIMIT 0,10
Marco Marsala
5

Sé que esta pregunta es antigua, pero ha recibido mucha atención a lo largo de los años y creo que le falta un concepto que pueda ayudar a alguien en un caso similar. Lo agrego aquí en aras de la integridad.

Si no puede modificar el esquema de su base de datos original, entonces se han proporcionado muchas buenas respuestas y resuelve el problema sin problemas.

Sin embargo, si puede modificar su esquema, le aconsejo que agregue un campo en su customertabla que contenga idel último customer_dataregistro de este 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
);

Consultar clientes

La consulta es tan fácil y rápida como puede ser:

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

El inconveniente es la complejidad adicional al crear o actualizar un cliente.

Actualización de un cliente

Siempre que desee actualizar un cliente, inserte un nuevo registro en la customer_datatabla y actualice el customerregistro.

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;

Creando un cliente

Crear un cliente es solo una cuestión de insertar la customerentrada y luego ejecutar las mismas declaraciones:

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;

Terminando

La complejidad adicional para crear / actualizar un cliente puede ser temible, pero se puede automatizar fácilmente con activadores.

Finalmente, si está usando un ORM, esto puede ser muy fácil de administrar. El ORM puede encargarse de insertar los valores, actualizar los identificadores y unir las dos tablas automáticamente por usted.

Así es como Customerse vería su modelo 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();
    }
}

Y su CustomerDatamodelo inmutable , que solo contiene captadores:

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;
    }
}
Benjamín
fuente
Combiné este enfoque con la solución de @ payne8 (arriba) para obtener el resultado deseado sin ninguna subconsulta.
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%' 

Creo que necesitas cambiar c.customer_id a c.id

else actualizar la estructura de la tabla

Pramendra Gupta
fuente
He votado en contra porque leí mal tu respuesta e inicialmente pensé que estaba mal. La prisa es una mala consejera :-)
Wirone
1

También puedes hacer esto

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
fuente
0

Es una buena idea registrar los datos reales en la tabla " customer_data ". Con estos datos puede seleccionar todos los datos de la tabla "customer_data" como desee.

Burçin
fuente