MySQL JOIN apenas a linha mais recente?

103

Eu tenho um cliente de tabela que armazena um customer_id, email e referência. Existe uma tabela adicional customer_data que armazena um registro histórico das alterações feitas ao cliente, ou seja, quando há uma alteração feita uma nova linha é inserida.

Para exibir as informações do cliente em uma tabela, as duas tabelas precisam ser unidas; no entanto, apenas a linha mais recente de customer_data deve ser unida à tabela do cliente.

Fica um pouco mais complicado porque a consulta é paginada e, portanto, tem um limite e um deslocamento.

Como posso fazer isso com o MySQL? Acho que estou querendo colocar um DISTINTO em algum lugar ...

A consulta no minuto é assim-

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

Além disso, estou certo em pensar que posso usar CONCAT com LIKE dessa forma?

(Compreendo que INNER JOIN pode ser o tipo errado de JOIN para usar. Na verdade, não tenho ideia de qual é a diferença entre os diferentes JOINs. Vou analisar isso agora!)

bcmcfc
fonte
Como é a tabela de histórico do cliente? Como a linha mais recente é determinada? Existe um campo de carimbo de data / hora?
Daniel Vassallo,
Mais recente é simplesmente a última linha inserida - portanto, sua chave primária é o número mais alto.
bcmcfc 01 de
Por que não um gatilho? dê uma olhada nesta resposta: stackoverflow.com/questions/26661314/…
Rodrigo Polo
A maioria / todas as respostas estavam demorando muito, com milhões de linhas. Existem algumas soluções com melhor desempenho.
Halil Özgür

Respostas:

142

Você pode tentar o seguinte:

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;

Observe que a JOINé apenas um sinônimo de INNER JOIN.

Caso de teste:

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 sem 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)
Daniel Vassallo
fonte
2
Obrigado pelo nível de detalhe que você analisou. Espero que ajude outras pessoas e apenas a mim!
bcmcfc 01 de
21
A longo prazo, essa abordagem pode criar problemas de desempenho, pois seria necessário criar uma tabela temporária. Portanto, outra solução (se possível) é adicionar um novo campo booleano (is_last) em customer_data que você teria que atualizar toda vez que uma nova entrada fosse adicionada. A última entrada terá is_last = 1, todos os outros para este cliente - is_last = 0.
cephuo
5
As pessoas deveriam (por favor) também ler a seguinte resposta (de Danny Coulombe), porque essa resposta (desculpe Daniel) é terrivelmente lenta com consultas mais longas / mais dados. Fiz minha página "esperar" 12 segundos para carregar; Portanto, verifique também stackoverflow.com/a/35965649/2776747 . Eu não percebi isso até depois de muitas outras mudanças, então demorei muito para descobrir.
Art
Você não tem ideia do quanto isso me ajudou :) Obrigado mestre
node_man
103

Se você estiver trabalhando com consultas pesadas, é melhor mover a solicitação para a linha mais recente na cláusula where. É muito mais rápido e parece mais limpo.

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
fonte
4
Uau, estou quase sem acreditar na diferença de desempenho que isso representa. Ainda não sei por que isso foi tão drástico, mas até agora foi tão mais rápido que parece que eu errei em outro lugar ...
Brian Leishman,
2
Eu realmente gostaria de poder marcar isto com +1 mais de uma vez para que seja visto mais. Eu testei isso um pouco e de alguma forma torna minhas consultas virtualmente instantâneas (o WorkBench literalmente diz 0,000 segundos, mesmo com sql_no_cache set), ao passo que fazer a pesquisa na junção levou vários segundos para ser concluída. Ainda perplexo, mas quero dizer que você não pode contestar resultados como esse.
Brian Leishman
1
Você está ingressando diretamente em 2 tabelas primeiro e depois filtrando com WHERE. Acho que é um grande problema de desempenho se você tem um milhão de clientes e dezenas de milhões de histórico de chamadas. Porque o SQL tentará unir 2 tabelas primeiro e depois filtrar até o único cliente. Prefiro filtrar os clientes e históricos de chamadas relacionados das tabelas primeiro em uma subconsulta e, em seguida, unir as tabelas.
Tarik
1
Suponho que "ca.client_id" e "ca.cal_event_id" devam ser "c" para ambos.
Herbert Van-Vliet
1
Eu concordo com @NickCoons. Valores NULL não serão retornados porque foram excluídos pela cláusula where. Como você faria para incluir os valores NULL e ainda manter o excelente desempenho desta consulta?
aanders77
10

Presumindo que a coluna de incremento automático em customer_dataseja nomeada Id, você pode fazer:

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

Para quem precisa trabalhar com uma versão mais antiga do MySQL (pré-5.0 ish), você não pode fazer subconsultas para este tipo de consulta. Aqui está a solução que consegui fazer e parecia funcionar muito bem.

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;

Essencialmente, isso é encontrar o id máximo de sua tabela de dados unindo-o ao cliente e, em seguida, unindo a tabela de dados ao id máximo encontrado. A razão para isso é porque selecionar o máximo de um grupo não garante que o resto dos dados correspondam ao id, a menos que você os junte de volta a si mesmos.

Eu não testei isso em versões mais recentes do MySQL, mas funciona em 4.0.30.

payne8
fonte
Isso é primoroso em sua simplicidade. Por que esta é a primeira vez que vejo essa abordagem? Observe que EXPLAINindica que isso usa uma tabela e um tipo de arquivo temporários. Adicionar ORDER BY NULLno final elimina o tipo de arquivo.
Timo
Para minha tristeza, minha própria solução não tão bonita é 3,5 vezes mais rápida para meus dados. Usei uma subconsulta para selecionar a tabela principal mais os IDs mais recentes das tabelas unidas e, em seguida, uma consulta externa que seleciona a subconsulta e lê os dados reais das tabelas unidas. Estou juntando 5 tabelas na tabela principal e testando com uma condição where que seleciona 1000 registros. Os índices são ótimos.
Timo
Eu estava usando sua solução com SELECT *, MAX(firstData.id), MAX(secondData.id) [...]. Logicamente, ao mudar para, SELECT main.*, firstData2.*, secondData2.*, MAX(firstData.id), MAX(secondData.id), [...]fui capaz de torná-lo significativamente mais rápido. Isso permite que as primeiras junções leiam apenas do índice, em vez de também ter que ler todos os dados do índice primário. Agora, a solução bonita leva apenas 1,9 vezes mais do que a solução baseada em subconsulta.
Timo
Ele não funciona mais no MySQL 5.7. Agora d2. * Retornará dados para a primeira linha do grupo, não para a última. SELECT MAX (R1.id), R2. * DAS faturas I LEFT JOIN respostas R1 ON I.id = R1.invoice_id LEFT JOIN respostas R2 ON R1.id = R2.id GROUP BY I.id LIMIT 0,10
Marco Marsala
5

Sei que essa pergunta é antiga, mas tem recebido muita atenção ao longo dos anos e acho que está faltando um conceito que pode ajudar alguém em um caso semelhante. Estou adicionando aqui para fins de integridade.

Se você não pode modificar o esquema do banco de dados original, então várias boas respostas foram fornecidas e resolvem o problema perfeitamente.

Se você puder , no entanto, modificar seu esquema, aconselho adicionar um campo em sua customertabela que contenha o iddo último customer_dataregistro deste 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
);

Consultando clientes

Consultar é tão fácil e rápido quanto pode 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 ...;

A desvantagem é a complexidade extra ao criar ou atualizar um cliente.

Atualizando um cliente

Sempre que você deseja atualizar um cliente, você insere um novo registro na customer_datatabela e atualiza o 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;

Criação de um cliente

Criar um cliente é apenas uma questão de inserir a customerentrada e, em seguida, executar as mesmas instruções:

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;

Empacotando

A complexidade extra para criar / atualizar um cliente pode ser assustadora, mas pode ser facilmente automatizada com gatilhos.

Finalmente, se você estiver usando um ORM, isso pode ser muito fácil de gerenciar. O ORM pode se encarregar de inserir os valores, atualizar os ids e juntar as duas tabelas automaticamente para você.

Esta é a aparência do seu Customermodelo mutável :

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 seu CustomerDatamodelo imutável , que contém apenas 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
fonte
Combinei essa abordagem com a solução de @ payne8 (acima) para obter o resultado desejado sem nenhuma subconsulta.
Ginger e 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%' 

eu acho que você precisa mudar c.customer_id para c.id

senão atualizar a estrutura da tabela

Pramendra Gupta
fonte
Eu votei negativamente porque interpretei mal sua resposta e inicialmente pensei que ela estava errada. Haste é um péssimo conselheiro :-)
Wirone
1

Você também pode fazer isso

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

É uma boa ideia registrar os dados reais na tabela " customer_data ". Com esses dados você pode selecionar todos os dados da tabela "customer_data" como desejar.

Burçin
fonte