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

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

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

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

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

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

+259
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.


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

+404
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

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

+109
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
   )
)
+22
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 

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

+13
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]
+6
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
источник

Если вы используете 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.

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

+2
20 июн. '18 в 5:08
источник

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

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
источник

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