Об'єднання до SQL: вибір останніх записів у взаємозв'язку один на багато


298

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

Будь ласка, використовуйте ці імена таблиці / стовпців у своїй відповіді:

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

І в більш складних ситуаціях, чи буде вигідним денормалізувати базу даних, помістивши останню покупку в таблицю клієнтів?

Якщо ідентифікатор (купівля) гарантовано буде відсортований за датою, чи можна спростити висловлювання, використовуючи щось на зразок LIMIT 1?


Так, це, можливо, варто денормалізувати (якщо це значно покращить продуктивність, про що можна дізнатися лише тестуючи обидві версії). Але недоліків денормалізації зазвичай варто уникати.
Вінс Боудрен

Відповіді:


449

Це приклад 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). Це може дозволити зробити зовнішнє з'єднання за допомогою індексу покриття. Не забудьте перевірити свою платформу, оскільки оптимізація залежить від впровадження. Використовуйте функції вашого RDBMS для аналізу плану оптимізації. Наприклад, EXPLAINна MySQL.


Деякі люди використовують підзапити замість рішення, яке я показую вище, але я вважаю, що моє рішення полегшує розв’язання зв’язків.


3
Вигідно, загалом. Але це залежить від марки використовуваної вами бази даних та кількості та розповсюдження даних у вашій базі даних. Єдиний спосіб отримати точну відповідь - це ви протестувати обидва рішення проти своїх даних.
Білл Карвін

27
Якщо ви хочете включити клієнтів, які ніколи не здійснювали покупки, то змініть ПРИЄДНАЙТЕ покупку p1 ON (c.id = p1.customer_id) на LEFT JOIN покупку p1 ON (c.id = p1.customer_id)
GordonM

5
@russds, вам потрібен унікальний стовпець, який ви можете використати для вирішення краватки. Немає сенсу мати два однакові рядки у реляційній базі даних.
Білл Карвін

6
Яка мета "ДЕ p2.id NULL"?
clu

3
це рішення працює лише у випадку, якщо є більше 1 записів про покупку. ist є посилання 1: 1, воно НЕ працює. там це повинно бути "ДЕ (p2.id IS NULL або p1.id = p2.id)
Бруно Дженріх

126

Ви також можете спробувати це зробити, використовуючи підбір

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

Вибір повинен приєднатися до всіх клієнтів та їх останньої дати покупки.


4
Завдяки цьому я просто врятував мене - це рішення здається більш надійним та ретельним, ніж інші, перелічені + його не специфічно для продукту
Daveo

Як я можу змінити це, якби хотів отримати клієнта, навіть якщо не було покупок?
clu

3
@clu: змініть INNER JOINна a LEFT OUTER JOIN.
Саша Чедигов

3
Схоже, це передбачає, що в цей день є лише одна покупка. Якби їх було два, ви б отримали два вихідні ряди для одного клієнта, я думаю?
artfulrobot

1
@IstiaqueAhmed - останній ПРИЄДНАННЯ ВНУТРІШНЯ приймає це значення Max (дата) і прив'язує його до вихідної таблиці. Без цього приєднання, єдиною інформацією, яку ви мали б у purchaseтаблиці, є дата та customer_id, але запит запитує всі поля з таблиці.
Сміх Вергілія

26

Ви не вказали базу даних. Якщо він дозволяє аналітичним функціям, можливо, цей підхід скоріше використовувати, ніж GROUP BY (безумовно, швидше в Oracle, швидше за все, швидше у пізніх версіях SQL Server, не знаю про інших).

Синтаксис у SQL сервері буде таким:

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

10
Це неправильна відповідь на питання, оскільки ви використовуєте "RANK ()" замість "ROW_NUMBER ()". RANK все одно надасть вам ту саму проблему зв’язків, коли дві покупки мають точно таку ж дату. Ось що робить функція ранжирування; якщо збігаються топ-2, їм обом присвоюється значення 1, а третій запис отримує значення 3. З Row_Number немає прив'язки, це унікально для всього розділу.
MikeTeeVee

4
Спробувавши підхід Білла Карвіна проти підходу Мадаліни, з планами виконання, включеними на сервері sql 2008, я виявив, що програма Білла Карвіна мала вартість запиту в 43% на відміну від підходу Мадаліни, який використовував 57% - тому, незважаючи на більш елегантний синтаксис цієї відповіді, я все-таки віддасть перевагу версії Білла!
Шосон

26

Іншим підходом було б використання 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
   )
)

Чи можете ви пояснити AND NOT EXISTSчастину простими словами?
Істяк Ахмед

Підселектор просто перевіряє, чи є рядок із вищим ідентифікатором. Ви отримаєте рядок у своєму наборі результатів, якщо не знайдено жодного з вищим ідентифікатором. Це має бути унікальним найвищим.
Стефан Хаберль

2
Це для мене найбільш читабельне рішення. Якщо це важливо.
fguillen

:) Дякую. Я завжди прагну до найбільш читаного рішення, тому що це важливо.
Стефан

19

Я знайшов цю тему як вирішення своєї проблеми.

Але коли я спробував їх, ефективність була низькою. Нижче - моя пропозиція щодо кращої роботи.

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 

Сподіваюся, що це буде корисно.


щоб отримати лише 1 я використав top 1та ordered it byMaxDatedesc
Roshna Omer

1
це просто і просто рішення, у моєму випадку (багато клієнтів, кілька покупок) на 10% швидше, ніж рішення @Stefan Haberl і більш ніж у 10 разів краще, ніж прийнята відповідь
Juraj Bezručka

Чудова пропозиція, що використовує загальні вирази таблиць (CTE) для вирішення цієї проблеми. Це значно покращило ефективність запитів у багатьох ситуаціях.
AdamsTips

Найкраща відповідь imo, легка для читання, стаття MAX () дає великі показники порівняно з ЗАМОВЛЕННЯ НА +
ГРАНИЦІ

10

Якщо ви використовуєте 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пункті.

Caveat: Це нестандартне застереження.


8

Спробуйте це, це допоможе.

Я використовував це у своєму проекті.

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]

Звідки береться псевдонім "р"?
TiagoA

це не працює добре .... взяв назавжди там, де інші приклади тут зайняли 2 секунди на наборі даних, які я маю ....
Joel_J

3

Тестовано на 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, дата, [будь-які інші стовпці покупки, які ви хочете повернути у виборі]).

LEFT OUTER JOIN(На відміну від INNER JOIN) буде переконатися , що клієнти , які ніколи не робили покупки, також включені.


не запускатимуться в t-sql, оскільки у
виділеному

1

Спробуйте,

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;
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.