SQL join: wybieranie ostatnich rekordów w relacji jeden do wielu

Przypuśćmy, że mam tabelę klientów i tabelę zakupów. Każdy zakup należy do jednego klienta. Chcę otrzymać listę wszystkich klientów wraz z ich ostatnim zakupem w jednym SELECT oświadczeniu. Jaka jest najlepsza praktyka? Jakieś rady na temat indeksów budowlanych?

Proszę użyć nazw tabel/kolumn w odpowiedzi:

  • klient: id, name
  • zakup: id, customer_id, item_id, date

I w bardziej skomplikowanych sytuacjach, czy czy (pod względem wydajności) korzystne jest denormalizowanie bazy danych poprzez umieszczenie ostatniego zakupu w tabeli klientów?

Jeśli (zakup) id jest gwarantowane, aby być sortowane według daty, można uprościć wyciągi używając czegoś takiego jak LIMIT 1?

Author: David Ferenczy Rogožan, 2010-01-21

10 answers

Jest to przykład problemu greatest-n-per-group, który pojawia się regularnie w StackOverflow.

Oto jak zwykle polecam rozwiązanie:

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;

Explanation: given a row p1, there should be no row p2 with the same customer and a later date (or in the case of ties, a later id). Kiedy okaże się, że to prawda, to p1 jest ostatnim zakupem dla tego klienta.

Jeśli chodzi o indeksy, utworzyłbym indeks złożony w purchase nad kolumnami (customer_id, date, id). Pozwala to na wykonanie połączenia zewnętrznego przy użyciu indeksu pokrycia. Pamiętaj, aby przetestować na swojej platformie, ponieważ optymalizacja zależy od wdrożenia. Użyj funkcji RDBMS do analizy planu optymalizacji. Np. {[10] } na MySQL.


Niektórzy używają zapytań podrzędnych zamiast rozwiązania, które pokazałem powyżej, ale uważam, że moje rozwiązanie ułatwia rozwiązywanie powiązań.

 505
Author: Bill Karwin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-08-25 02:23:41

Możesz również spróbować zrobić to za pomocą 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

Wybierz powinien dołączyć do wszystkich klientów i ich Ostatnia Data zakupu.

 142
Author: Adriaan Stander,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-21 17:40:54

Innym podejściem byłoby użycie warunku NOT EXISTS w Twoim warunku przyłączenia do testów dla późniejszych zakupów:

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
   )
)
 34
Author: Stefan Haberl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-12 08:21:00

Nie podałeś bazy danych. Jeśli jest to takie, które umożliwia funkcje analityczne, może być szybsze użycie tego podejścia niż grupa po jednym(zdecydowanie szybciej w Oracle, najprawdopodobniej szybciej w późnych edycjach SQL Server, Nie wiem o innych).

Składnia w SQL Server będzie:

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
 29
Author: Madalina Dragomir,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-21 18:23:30

Znalazłem ten wątek jako rozwiązanie mojego problemu.

Ale kiedy próbowałem ich wydajność była niska. Poniżej jest moja propozycja dla lepszej wydajności.
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 
Mam nadzieję, że to będzie pomocne.
 22
Author: Mathee,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-10-23 06:29:29

Jeśli używasz PostgreSQL, możesz użyć DISTINCT ON, aby znaleźć pierwszy wiersz w grupie.

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 Docs-Distinct On

Zauważ, że DISTINCT ON pole(pola) -- here customer_id -- musi pasować do lewej większości pól w ORDER BY klauzuli.

Zastrzeżenie: jest to klauzula niestandardowa.

 17
Author: Tate Thurston,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-21 21:40:21

Spróbuj tego, to pomoże.

Użyłem tego w moim projekcie.

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]
 8
Author: Rahul Murari,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-05-23 19:27:02

Testowane na 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

Funkcja agregująca max() upewni się, że ostatni zakup zostanie wybrany z każdej grupy (ale zakłada, że kolumna daty jest w formacie, w którym max() podaje ostatni - co zwykle ma miejsce). Jeśli chcesz obsługiwać zakupy z tą samą datą, możesz użyć max(p.date, p.id).

Jeśli chodzi o indeksy, użyłbym indeksu przy zakupie z (customer_id, date, [inne kolumny zakupu, które chcesz zwrócić w swoim select]).

The LEFT OUTER JOIN (w przeciwieństwie do INNER JOIN) upewni się, że klienci, którzy nigdy nie dokonali zakupu, są również uwzględniani.

 3
Author: Mark,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-27 04:39:28

Proszę spróbować tego,

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
Author: Milad Shahbazi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-06-25 09:50:51

Nie wchodząc najpierw w kod, logika / algorytm idzie poniżej:

  1. Przejdź do tabeli transaction z wieloma rekordami dla tego samego client.

  2. Wybierz rekordy clientID i latestDate aktywności klienta za pomocą group by clientID i max(transactionDate)

       select clientID, max(transactionDate) as latestDate 
       from transaction 
       group by clientID
    
  3. inner join Tabela transaction z wynikiem z Kroku 2, wtedy będziesz miał pełne rekordy tabeli transaction z tylko najnowszym rekordem każdego klienta.

       select * from 
       transaction t 
       inner join (
         select clientID, max(transactionDate) as latestDate
         from transaction 
         group by clientID) d 
       on t.clientID = d.clientID and t.transactionDate = d.latestDate) 
    
  4. Możesz użyć wynik z kroku 3, aby dołączyć do dowolnej tabeli, którą chcesz uzyskać różne wyniki.

 0
Author: Gary Bao Yutong 鲍昱彤,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-10-05 20:27:34