Соединение 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;