MySQL INNER JOIN seleciona apenas uma linha da segunda tabela

104

Eu tenho uma usersmesa e uma paymentsmesa, para cada usuário, aqueles que possuem pagamentos, podem ter vários pagamentos associados na paymentsmesa. Gostaria de selecionar todos os usuários que têm pagamentos, mas selecionar apenas o último pagamento. Estou tentando este SQL, mas nunca tentei instruções SQL aninhadas antes, então quero saber o que estou fazendo de errado. Agradeço a ajuda

SELECT u.* 
FROM users AS u
    INNER JOIN (
        SELECT p.*
        FROM payments AS p
        ORDER BY date DESC
        LIMIT 1
    )
    ON p.user_id = u.id
WHERE u.package = 1
Wasim
fonte

Respostas:

148

Você precisa ter uma subconsulta para obter a data mais recente por user ID.

SELECT  a.*, c.*
FROM users a 
    INNER JOIN payments c
        ON a.id = c.user_ID
    INNER JOIN
    (
        SELECT user_ID, MAX(date) maxDate
        FROM payments
        GROUP BY user_ID
    ) b ON c.user_ID = b.user_ID AND
            c.date = b.maxDate
WHERE a.package = 1
John Woo
fonte
1
@Fluffeh você tem uma resposta muito boa Part 1 - Joins and Unions. :) marcado como favorito!
John Woo
1
Obrigado, colega, escrevi exatamente para esse tipo de situação, exiba uma resposta rápida com o SQL correto e, em seguida, sugira um link para aquele monstro lido em detalhes para que as pessoas possam entender o código sugerido. Planejando adicioná-lo para expandi-lo ainda mais. Bem-vindo a participar, se quiser, devo abrir um violino para o código realmente ...
Fluffeh
@JohnWoo obrigado pela sua resposta, funcionou perfeitamente. E obrigado Fluffeh pelo Q&A, vou dar uma olhada nisso!
Wasim
Acho que minha resposta é mais simples e mais rápida porque estou usando apenas uma junção interna. Talvez eu esteja errado?
Mihai Matei
2
Existe uma maneira de fazer essa consulta sem as junções internas? Quero retornar o máximo da tabela c OU obter um valor nulo de volta se nenhuma linha corresponder. Alterar JOIN para LEFT JOIN não funciona obviamente.
Scott
32
SELECT u.*, p.*
FROM users AS u
INNER JOIN payments AS p ON p.id = (
    SELECT id
    FROM payments AS p2
    WHERE p2.user_id = u.id
    ORDER BY date DESC
    LIMIT 1
)

Esta solução é melhor do que a resposta aceita porque funciona corretamente quando há alguns pagamentos com o mesmo usuário e data.

Finesse
fonte
é uma boa abordagem que você usa. mas tenho dúvidas sobre como buscar uma imagem em um produto similar. Um produto tem muitas (4) imagens, mas quero mostrar apenas uma imagem desse produto.
Ali Raza
Você não acha que vai demorar muito para usar? já que você está processando cada registro em sua subconsulta para junção interna. A resposta aceita pode ser a melhor resposta possível após um pequeno ajuste.
Hamees A. Khan
@ HameesA.Khan, não examinei a execução das consultas. Você pode estar certo, mas a solução aceita faz uma junção tridimensional que também pode ser lenta. Receio que o ajuste não será menor (este é o assunto da pergunta).
Finesse
12
SELECT u.*, p.*, max(p.date)
FROM payments p
JOIN users u ON u.id=p.user_id AND u.package = 1
GROUP BY u.id
ORDER BY p.date DESC

Confira este sqlfiddle

Mihai Matei
fonte
6
A limit 1cláusula retornará apenas 1 usuário, o que não é o que o OP deseja.
Fluffeh
6
@MateiMihai, isso não funciona. A consulta fornece apenas a data máxima, não a linha inteira com a data máxima. Você pode ver isso no violino: a datecoluna é diferente de max(p.date). Se você adicionar mais colunas na paymentstabela (por exemplo cost), todas as colunas não serão da linha necessária
zxcat
3
   SELECT u.* 
        FROM users AS u
        INNER JOIN (
            SELECT p.*,
             @num := if(@id = user_id, @num + 1, 1) as row_number,
             @id := user_id as tmp
            FROM payments AS p,
                 (SELECT @num := 0) x,
                 (SELECT @id := 0) y
            ORDER BY p.user_id ASC, date DESC)
        ON (p.user_id = u.id) and (p.row_number=1)
        WHERE u.package = 1
valex
fonte
2

Existem dois problemas com a sua consulta:

  1. Cada tabela e subconsulta precisa de um nome, então você deve nomear a subconsulta INNER JOIN (SELECT ...) AS p ON ....
  2. A subconsulta, conforme você a possui, retorna apenas um período de linha, mas você realmente deseja uma linha para cada usuário . Para isso, você precisa de uma consulta para obter a data máxima e, em seguida, unir-se novamente para obter a linha inteira.

Supondo que não haja empates para payments.date, tente:

    SELECT u.*, p.* 
    FROM (
        SELECT MAX(p.date) AS date, p.user_id 
        FROM payments AS p
        GROUP BY p.user_id
    ) AS latestP
    INNER JOIN users AS u ON latestP.user_id = u.id
    INNER JOIN payments AS p ON p.user_id = u.id AND p.date = latestP.date
    WHERE u.package = 1
lc.
fonte
é uma boa abordagem que você usa. mas tenho dúvidas sobre como buscar uma imagem em um produto similar. Um produto tem muitas (4) imagens, mas quero mostrar apenas uma imagem desse produto.
Ali Raza
Achei isso mais rápido no meu caso. A única alteração que fiz foi adicionar uma cláusula where na subconsulta para filtrar os dados do usuário selecionado apenas From payments as p Where p.user_id =@user_idquando a consulta estiver agrupando por em toda a tabela.
Vikash Rathee
2

A resposta de @John Woo me ajudou a resolver um problema semelhante. Eu melhorei sua resposta definindo a ordem correta também. Isso tem funcionado para mim:

SELECT  a.*, c.*
FROM users a 
    INNER JOIN payments c
        ON a.id = c.user_ID
    INNER JOIN (
        SELECT user_ID, MAX(date) as maxDate FROM
        (
            SELECT user_ID, date
            FROM payments
            ORDER BY date DESC
        ) d
        GROUP BY user_ID
    ) b ON c.user_ID = b.user_ID AND
           c.date = b.maxDate
WHERE a.package = 1

Não tenho certeza de quão eficiente isso é, no entanto.

GTCrais
fonte
2
SELECT U.*, V.* FROM users AS U 
INNER JOIN (SELECT *
FROM payments
WHERE id IN (
SELECT MAX(id)
FROM payments
GROUP BY user_id
)) AS V ON U.id = V.user_id

Isso fará com que funcione

Justice Eziefule
fonte
1

Matei Mihai forneceu uma solução simples e eficiente, mas não funcionará até que seja colocado um MAX(date)na parte SELECT, então esta consulta se tornará:

SELECT u.*, p.*, max(date)
FROM payments p
JOIN users u ON u.id=p.user_id AND u.package = 1
GROUP BY u.id

E ordenar por não fará nenhuma diferença no agrupamento, mas pode ordenar o resultado final fornecido por grupo por. Eu tentei e funcionou para mim.

Hassan Dad Khan
fonte
1

Minha resposta foi inspirada diretamente em @valex, muito útil, se você precisar de várias colunas na cláusula ORDER BY.

    SELECT u.* 
    FROM users AS u
    INNER JOIN (
        SELECT p.*,
         @num := if(@id = user_id, @num + 1, 1) as row_number,
         @id := user_id as tmp
        FROM (SELECT * FROM payments ORDER BY p.user_id ASC, date DESC) AS p,
             (SELECT @num := 0) x,
             (SELECT @id := 0) y
        )
    ON (p.user_id = u.id) and (p.row_number=1)
    WHERE u.package = 1
Jérôme B
fonte
1

Você pode tentar isto:

SELECT u.*, p.*
FROM users AS u LEFT JOIN (
    SELECT *, ROW_NUMBER() OVER(PARTITION BY userid ORDER BY [Date] DESC) AS RowNo
    FROM payments  
) AS p ON u.userid = p.userid AND p.RowNo=1
Shekhar
fonte
1
Embora este snippet de código possa resolver a questão, incluir uma explicação realmente ajuda a melhorar a qualidade de sua postagem. Lembre-se de que você está respondendo à pergunta para leitores no futuro e essas pessoas podem não saber os motivos de sua sugestão de código.
Alessio