web-dev-qa-db-ja.com

SQL結合:一対多の関係で最後のレコードを選択する

顧客のテーブルと購入のテーブルがあるとします。各購入は1人の顧客に属します。私は1つのSELECTステートメントで彼らの最後の購入とともにすべての顧客のリストを得たいです。ベストプラクティスは何ですか?索引作成に関するアドバイスはありますか?

あなたの答えにこれらのテーブル/カラム名を使ってください:

  • 顧客:ID、名前
  • 購入:id、customer_id、item_id、日付

そしてもっと複雑な状況で、最後の購入をcustomerテーブルに入れることによってデータベースを非正規化することは(パフォーマンス的に)有益でしょうか?

(購入)IDが日付順にソートされていることが保証されている場合、LIMIT 1のようなものを使用してステートメントを単純化できますか?

239
netvope

これはStackOverflowで定期的に発生しているgreatest-n-per-group問題の例です。

これが私が通常それを解決することをお勧めする方法です:

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がその顧客の最新の購入です。

インデックスに関しては、列(customer_idpurchasedate)の上のidに複合インデックスを作成します。これにより、カバーインデックスを使用して外部結合を実行できます。最適化は実装に依存するため、必ずプラットフォームでテストしてください。最適化計画を分析するには、RDBMSの機能を使用してください。例えば。 MySQLではEXPLAIN


私が上で示した解決策の代わりにサブクエリを使う人もいますが、私の解決策は関係を解決するのをより簡単にすると思います。

378
Bill Karwin

サブセレクトを使ってこれを試すこともできます

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

Selectは、すべての顧客とその顧客のLast購入日に参加する必要があります。

106
Adriaan Stander

データベースを指定していません。分析機能を可能にするものであれば、GROUP BYのものよりもこのアプローチを使用するほうが速い場合があります(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

もう1つの方法は、結合条件に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
Stefan Haberl

私は私の問題に対する解決策としてこのスレッドを見つけました。

しかし、私がそれらを試したとき、パフォーマンスは低かった。ベローは、より良いパフォーマンスを得るための私の提案です。

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 

これが役立つことを願っています。

11
Mathee

これを試してください、それは役立ちます。

私は自分のプロジェクトでこれを使用しました。

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
Rahul Murari

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、[あなたがあなたのselectに返したい他の購入コラム])で購入時のインデックスを使います。

LEFT OUTER JOININNER JOINとは対照的に)は、購入したことがない顧客も確実に含まれるようにします。

3
Mark

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
Tate Thurston

これを試してください、

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
Milad Shahbazi