Соединение SQL: выбор последних записей в отношениях "один ко многим"

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

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

  • клиент: id, имя
  • покупка: id, customer_id, item_id, дата

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

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

+235
21 янв. '10 в 17:29
источник поделиться
9 ответов

Это пример проблемы 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.


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

+375
21 янв. '10 в 17:35
источник

Связанные вопросы


Похожие вопросы

Вы также можете попробовать сделать это, используя 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

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

+105
21 янв. '10 в 17:40
источник

Вы не указали базу данных. Если это тот, который позволяет аналитические функции, он может использовать этот подход быстрее, чем 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
+24
21 янв. '10 в 18:23
источник

Другим подходом было бы использовать условие 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
   )
)
+18
12 июн. '13 в 8:10
источник

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

Но когда я их пробовал, производительность была низкой. 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 

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

+10
23 окт. '14 в 6:29
источник

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

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

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]
+4
16 янв. '18 в 5:07
источник

Протестировано на 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) обеспечит включение клиентов, которые никогда не делали покупки.

+3
27 янв. '18 в 4:25
источник

Попробуйте это,

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;
+1
25 июн. '16 в 9:25
источник

Если вы используете 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
20 июн. '18 в 5:08
источник

Посмотрите другие вопросы по меткам или Задайте вопрос