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


298

顧客の表と購入の表があるとします。各購入は1人の顧客に属します。1つのSELECTステートメントで、すべての顧客のリストと最後の購入を取得したいと考えています。ベストプラクティスは何ですか?インデックスの構築に関するアドバイスはありますか?

回答ではこれらのテーブル/列名を使用してください:

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

さらに複雑な状況では、最後の購入を顧客テーブルに入れてデータベースを非正規化することは(パフォーマンスの観点から)有益でしょうか?

(購入)IDが日付順にソートされることが保証されている場合、ステートメントを簡素化できLIMIT 1ますか?


はい、非正規化する価値があります(パフォーマンスが大幅に向上する場合、両方のバージョンをテストすることによってのみ確認できます)。しかし、非正規化の欠点は通常、回避する価値があります。
Vince Bowdren 2010年

回答:


451

これは、greatest-n-per-groupStackOverflowで定期的に発生する問題の例です。

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

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_iddateid)。これにより、カバリングインデックスを使用して外部結合を行うことができます。最適化は実装に依存するため、必ずプラットフォームでテストしてください。RDBMSの機能を使用して、最適化計画を分析します。たとえばEXPLAINMySQL。


上に示したソリューションの代わりにサブクエリを使用する人もいますが、私のソリューションを使用すると、関係を簡単に解決できることがわかりました。


3
好意的には、一般的に。ただし、それは使用するデータベースのブランド、およびデータベース内のデータの量と分布によって異なります。正確な答えを得る唯一の方法は、データに対して両方のソリューションをテストすることです。
Bill Karwin、2010年

27
購入したことのない顧客を含める場合は、JOIN購入p1 ON(c.id = p1.customer_id)をLEFT JOIN購入p1 ON(c.id = p1.customer_id)に変更します
GordonM

5
@russds、タイを解決するために使用できるいくつかの一意の列が必要です。リレーショナルデータベースに2つの同じ行があることは意味がありません。
Bill Karwin、

6
「WHERE p2.id IS NULL」の目的は何ですか?
2015年

3
このソリューションは、複数の購入レコードがある場合にのみ機能します。1:1リンクがないと動作しません。そこでは「WHERE(p2.id IS NULLまたはp1.id = p2.id)
Bruno Jennrich

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

購入していなくても顧客を獲得したい場合、これをどのように変更しますか?
2015年

3
@clu:変更INNER JOINしますLEFT OUTER JOIN
Sasha Chedygov、2015年

3
これは、その日に購入が1つしかないことを前提としているようです。2つある場合、1人の顧客に対して2つの出力行が表示されると思いますか?
artfulrobot 2017年

1
@IstiaqueAhmed-最後のINNER JOINはそのMax(date)値を取り、それをソーステーブルに結び付けます。その結合がない場合、purchaseテーブルから得られる情報は日付とcustomer_idだけですが、クエリはテーブルのすべてのフィールドを要求します。
バージルを笑う

26

データベースが指定されていません。分析関数を許可するものである場合は、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

10
「ROW_NUMBER()」の代わりに「RANK()」を使用しているため、これは質問に対する間違った回答です。RANKは、2つの購入の日付がまったく同じである場合でも、同じ関係の問題を示します。これは、ランキング関数が行うことです。上位2が一致した場合、両方に値1が割り当てられ、3番目のレコードには値3が割り当てられます。Row_Numberを使用すると、タイはなく、パーティション全体で一意になります。
MikeTeeVee 2012年

4
ここでマダリーナのアプローチに対してビルカーウィンのアプローチを試してみると、SQLサーバー2008で実行プランが有効になっているため、57%を使用するマダリーナのアプローチとは対照的に、ビルカーウィンのアプローチのクエリコストは43%であることがわかりました。それでもビルのバージョンを支持します!
Shawson 2012

26

もう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
   )
)

そのAND NOT EXISTS部分を簡単な言葉で説明できますか?
Istiaque Ahmed 2017

副選択は、より高いIDの行があるかどうかを確認するだけです。より高いIDの行が見つからない場合にのみ、結果セットに行を取得します。それはユニークな最高のものでなければなりません。
Stefan Haberl 2017年

2
これは私にとって最も読みやすいソリューションです。これが重要な場合。
fguillen

:)ありがとう。それ重要なので、私は常に最も読みやすいソリューションを目指して努力しています。
Stefan Haberl、

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 1ordered it byMaxDateを取得するにはdesc
Roshna Omer

1
これは簡単でわかりやすいソリューションです。MYの場合(多くのお客様、数少ない購入)、@ Stefan Haberlのソリューションよりも10%速く、承認された回答よりも10倍以上優れています
JurajBezručkaMay

共通テーブル式(CTE)を使用してこの問題を解決することをお勧めします。これにより、多くの状況でクエリのパフォーマンスが劇的に向上しました。
AdamsTips 2018

ベストアンサーimo、読みやすいMAX()句は、ORDER BY + LIMIT 1
mrj

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句の左端のフィールドと一致する必要があります。

警告:これは非標準的な条項です。


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]

エイリアス「p」はどこから来たのですか?
TiagoA

このdoesntのは....ここで、他の例では、データに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(p.date, p.id)

インデックスに関しては、購入時に(customer_id、date、[selectに返したいその他の購入列])のインデックスを使用します。

LEFT OUTER JOIN(とは対照的にINNER JOIN)必ず購入を行ったことがない顧客も含まれていることを確認します。


select c。*にはgroup by句にない列があるため、t-sqlでは実行されません
Joel_J

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;
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.