Соединение SQL: выбор последних записей в отношениях "один ко многим"
Предположим, у меня есть таблица клиентов и таблица покупок. Каждая покупка принадлежит одному клиенту. Я хочу получить список всех клиентов вместе со своей последней покупкой в одном заявлении SELECT. Какова наилучшая практика? Любые советы по созданию индексов?
Пожалуйста, используйте эти имена таблиц и столбцов в ответе:
- клиент: id, имя
- покупка: id, customer_id, item_id, дата
И в более сложных ситуациях было бы (полезно) полезно денормализовать базу данных, поместив последнюю покупку в таблицу клиентов?
Если идентификатор (покупка) гарантированно будет отсортирован по дате, можно упростить эти утверждения, используя что-то вроде LIMIT 1
?
Ответы
Ответ 1
Это пример проблемы greatest-n-per-group
, которая регулярно появляется в Qaru.
Вот как я обычно рекомендую решить эту проблему:
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.
Некоторые люди используют подзапросы вместо решения, которое я показываю выше, но я считаю, что мое решение облегчает разрешение связей.
Ответ 2
Вы также можете попробовать сделать это, используя sub select
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
Выбор должен присоединиться ко всем клиентам и к их дате покупки Последняя.
Ответ 3
Вы не указали базу данных. Если это тот, который позволяет аналитические функции, он может использовать этот подход быстрее, чем GROUP BY one (определенно быстрее в 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
Ответ 4
Другим подходом было бы использовать условие 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
)
)
Ответ 5
Я нашел эту нить как решение моей проблемы.
Но когда я их пробовал, производительность была низкой. Bellow - мое предложение о лучшей производительности.
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
Надеюсь, это будет полезно.
Ответ 6
Попробуйте это, это поможет.
Я использовал это в своем проекте.
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]
Ответ 7
Протестировано на 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
) обеспечит включение клиентов, которые никогда не делали покупки.
Ответ 8
Если вы используете 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
.
Предостережение: это нестандартное предложение.
Ответ 9
Попробуйте это,
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;