SQL-соединение: выбор последних записей в отношении один-ко-многим - PullRequest
242 голосов
/ 21 января 2010

Предположим, у меня есть таблица клиентов и таблица покупок. Каждая покупка принадлежит одному клиенту. Я хочу получить список всех клиентов вместе с их последней покупкой в ​​одном операторе SELECT. Какова лучшая практика? Любой совет по созданию индексов?

Пожалуйста, используйте следующие имена таблиц / столбцов в своем ответе:

  • Заказчик: идентификатор, имя
  • покупка: id, customer_id, item_id, дата

А в более сложных ситуациях было бы (с точки зрения производительности) выгодно денормализовать базу данных, поместив последнюю покупку в таблицу клиентов?

Если идентификатор (покупки) гарантированно отсортирован по дате, можно ли упростить выписки, используя что-то вроде LIMIT 1?

Ответы [ 9 ]

384 голосов
/ 21 января 2010

Это пример проблемы greatest-n-per-group, которая регулярно появлялась в StackOverflow.

Вот как я обычно рекомендую ее решить:

SELECT c.*, p1.*
FROM customer c
JOIN purchase p1 ON (c.id = p1.customer_id)
LEFT OUTER JOIN purchase p2 ON (c.id = p2.customer_id AND 
    (p1.date < p2.date OR p1.date = p2.date AND p1.id < p2.id))
WHERE p2.id IS NULL;

Объяснение: для строки p1 не должно быть строки p2 с тем же клиентом и более поздней датой (или, в случае связей, более поздней id). Когда мы обнаруживаем, что это правда, то p1 является самой последней покупкой для этого клиента.

Что касается индексов, я бы создал составной индекс в purchase по столбцам (customer_id, date, id). Это может позволить сделать внешнее соединение с помощью индекса покрытия. Обязательно протестируйте на своей платформе, потому что оптимизация зависит от реализации. Используйте функции вашей РСУБД для анализа плана оптимизации. Например. EXPLAIN в MySQL.


Некоторые люди используют подзапросы вместо решения, которое я показываю выше, но я считаю, что мое решение облегчает разрешение связей.

106 голосов
/ 21 января 2010

Вы также можете попробовать сделать это с помощью дополнительного выбора

SELECT  c.*, p.*
FROM    customer c INNER JOIN
        (
            SELECT  customer_id,
                    MAX(date) MaxDate
            FROM    purchase
            GROUP BY customer_id
        ) MaxDates ON c.id = MaxDates.customer_id INNER JOIN
        purchase p ON   MaxDates.customer_id = p.customer_id
                    AND MaxDates.MaxDate = p.date

Выбор должен присоединиться ко всем клиентам и их Последняя дата покупки.

24 голосов
/ 21 января 2010

Вы не указали базу данных. Если это та функция, которая допускает аналитические функции, возможно, этот подход будет быстрее, чем метод GROUP BY (определенно быстрее в Oracle, скорее всего быстрее в поздних выпусках SQL Server, о других не знаю).

Синтаксис в SQL Server будет:

SELECT c.*, p.*
FROM customer c INNER JOIN 
     (SELECT RANK() OVER (PARTITION BY customer_id ORDER BY date DESC) r, *
             FROM purchase) p
ON (c.id = p.customer_id)
WHERE p.r = 1
18 голосов
/ 12 июня 2013

Другой подход заключается в использовании условия NOT EXISTS в вашем условии соединения для проверки последующих покупок:

SELECT *
FROM customer c
LEFT JOIN purchase p ON (
       c.id = p.customer_id
   AND NOT EXISTS (
     SELECT 1 FROM purchase p1
     WHERE p1.customer_id = c.id
     AND p1.id > p.id
   )
)
11 голосов
/ 23 октября 2014

Я нашел эту тему как решение моей проблемы.

Но когда я попробовал их, производительность была низкой. Сильфон - мое предложение для лучшей производительности.

With MaxDates as (
SELECT  customer_id,
                MAX(date) MaxDate
        FROM    purchase
        GROUP BY customer_id
)

SELECT  c.*, M.*
FROM    customer c INNER JOIN
        MaxDates as M ON c.id = M.customer_id 

Надеюсь, это будет полезно.

4 голосов
/ 16 января 2018

Попробуйте, это поможет.

Я использовал это в своем проекте.

SELECT 
*
FROM
customer c
OUTER APPLY(SELECT top 1 * FROM purchase pi 
WHERE pi.customer_id = c.Id order by pi.Id desc) AS [LastPurchasePrice]
3 голосов
/ 27 января 2018

Протестировано на SQLite:

SELECT c.*, p.*, max(p.date)
FROM customer c
LEFT OUTER JOIN purchase p
ON c.id = p.customer_id
GROUP BY c.id

Агрегатная функция max() обеспечит выбор последней покупки из каждой группы (но предполагает, что столбец даты имеет формат, в котором max () даетпоследний - что обычно бывает).Если вы хотите обрабатывать покупки с той же датой, то вы можете использовать max(p.date, p.id).

. Что касается индексов, я бы использовал индекс покупки с (customer_id, date, [любые другие столбцы покупки, которые вы хотитевернитесь к своему выбору]).

LEFT OUTER JOIN (в отличие от INNER JOIN) обеспечит включение клиентов, которые никогда не делали покупки.

1 голос
/ 20 июня 2018

Если вы используете PostgreSQL, вы можете использовать DISTINCT ON, чтобы найти первую строку в группе.

SELECT customer.*, purchase.*
FROM customer
JOIN (
   SELECT DISTINCT ON (customer_id) *
   FROM purchase
   ORDER BY customer_id, date DESC
) purchase ON purchase.customer_id = customer.id

Документы PostgreSQL - Различное вкл.

Обратите внимание, что поле (поля) DISTINCT ON - здесь customer_id - должно соответствовать крайнему левому полю (ам) в предложении ORDER BY.

Предостережение: это нестандартное предложение.

1 голос
/ 25 июня 2016

Пожалуйста, попробуйте это,

SELECT 
c.Id,
c.name,
(SELECT pi.price FROM purchase pi WHERE pi.Id = MAX(p.Id)) AS [LastPurchasePrice]
FROM customer c INNER JOIN purchase p 
ON c.Id = p.customerId 
GROUP BY c.Id,c.name;
...