Skip to content

Commit b2b5ae0

Browse files
committed
SQL: Basic Join 1068. Product Sales Analysis I
1 parent 7a76ecd commit b2b5ae0

3 files changed

Lines changed: 870 additions & 0 deletions

File tree

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "a4a02d79",
6+
"metadata": {},
7+
"source": [
8+
"## 0) 前提\n",
9+
"\n",
10+
"* エンジン: **MySQL 8.0.40**\n",
11+
"* 結合キー:\n",
12+
"\n",
13+
" * `Sales.product_id` ↔ `Product.product_id`\n",
14+
"* 要求出力:\n",
15+
"\n",
16+
" * 各 `sale_id` ごとに\n",
17+
" `product_name, year, price`\n",
18+
"* 集計・順位付けは不要なので **ウィンドウ関数は使わない方がシンプルで高速**\n",
19+
"\n",
20+
"---\n",
21+
"\n",
22+
"## 1) 問題整理\n",
23+
"\n",
24+
"* 入力テーブル\n",
25+
"\n",
26+
" **Sales**\n",
27+
"\n",
28+
" * `sale_id` (PK の一部: `sale_id, year`)\n",
29+
" * `product_id` (FK → `Product.product_id`)\n",
30+
" * `year`\n",
31+
" * `quantity`\n",
32+
" * `price`(単価)\n",
33+
"\n",
34+
" **Product**\n",
35+
"\n",
36+
" * `product_id` (PK)\n",
37+
" * `product_name`\n",
38+
"\n",
39+
"* 出力仕様\n",
40+
"\n",
41+
" * 列: `product_name, year, price`\n",
42+
" * 各行は `Sales` の 1 レコードに対応\n",
43+
" * 並び順は任意(`ORDER BY` なし)\n",
44+
"\n",
45+
"* 方針\n",
46+
"\n",
47+
" * 各売上行に対して、対応する商品の名前を `INNER JOIN` で取得し、そのまま投影\n",
48+
"\n",
49+
"---\n",
50+
"\n",
51+
"## 2) 最適解(単一クエリ)\n",
52+
"\n",
53+
"> 今回は「各行そのまま+名前付け」なので、**単純な内部結合のみ**が最短・最速です。\n",
54+
"> ウィンドウ関数や CTE による前処理は不要です。\n",
55+
"\n",
56+
"```sql\n",
57+
"SELECT\n",
58+
" p.product_name,\n",
59+
" s.year,\n",
60+
" s.price\n",
61+
"FROM Sales AS s\n",
62+
"JOIN Product AS p\n",
63+
" ON p.product_id = s.product_id;\n",
64+
"\n",
65+
"Runtime 1155 ms\n",
66+
"Beats 69.04%\n",
67+
"\n",
68+
"```\n",
69+
"\n",
70+
"* `JOIN`(= `INNER JOIN`)なので、`Product` に存在しない `product_id` は自然に除外\n",
71+
"* 並び順要件なし → `ORDER BY` を付与しないことで余計なソートコストを回避\n",
72+
"\n",
73+
"---\n",
74+
"\n",
75+
"## 3) 代替解\n",
76+
"\n",
77+
"### 3-1. 相関サブクエリ版(JOIN 不使用)\n",
78+
"\n",
79+
"JOIN が苦手な読者向けの書き方としては、以下のように **相関サブクエリ** も可能です(ただしパフォーマンス面では JOIN が優位)。\n",
80+
"\n",
81+
"```sql\n",
82+
"SELECT\n",
83+
" (\n",
84+
" SELECT p.product_name\n",
85+
" FROM Product AS p\n",
86+
" WHERE p.product_id = s.product_id\n",
87+
" ) AS product_name,\n",
88+
" s.year,\n",
89+
" s.price\n",
90+
"FROM Sales AS s;\n",
91+
"\n",
92+
"Runtime 5589 ms\n",
93+
"Beats 5.01%\n",
94+
"\n",
95+
"```\n",
96+
"\n",
97+
"* `Sales` 側の行ごとに `Product` テーブルを参照\n",
98+
"* インデックスがあれば許容されるが、大量データでは JOIN の方が一般的に高速\n",
99+
"\n",
100+
"---\n",
101+
"\n",
102+
"## 4) 要点解説\n",
103+
"\n",
104+
"1. **ID ベースでの結合**\n",
105+
"\n",
106+
" * 問題文が「`product_id` は `Product` の外部キー」と明示しているため、\n",
107+
"\n",
108+
" * **結合条件**: `Sales.product_id = Product.product_id`\n",
109+
" * 結果の正しさは ID ベースで保証される\n",
110+
"\n",
111+
"2. **集計・フィルタが不要**\n",
112+
"\n",
113+
" * 「各 sale_id について」情報を出すだけなので\n",
114+
"\n",
115+
" * 集約 (`GROUP BY`)\n",
116+
" * 順位付け (`ROW_NUMBER`, `RANK` など)\n",
117+
" * TOP k 抽出 (`LIMIT`)\n",
118+
" は一切不要 → **シンプルな SELECT + JOIN が最適**\n",
119+
"\n",
120+
"3. **`INNER JOIN` を選ぶ理由**\n",
121+
"\n",
122+
" * 問題文は「Sales テーブルの sale_id ごと」としか書いていないが、同時に `product_id` は `Product` の FK とあるので「整合データ前提」と解釈し、**商品マスタに存在しない ID をわざわざ残す必要はない** ⇒ `INNER JOIN` で十分\n",
123+
"\n",
124+
"4. **並び順任意 → `ORDER BY` 省略**\n",
125+
"\n",
126+
" * LeetCode 的には「any order」なら `ORDER BY` は不要\n",
127+
" * 省略によりソートステップがなくなり、クエリプランが軽くなる\n",
128+
"\n",
129+
"5. **インデックス利用**\n",
130+
"\n",
131+
" * 典型的には以下が用意されている想定\n",
132+
"\n",
133+
" * `Product(product_id)`(PK)\n",
134+
" * `Sales(product_id)`(FK 用インデックス)\n",
135+
" * これにより、`Sales` から `Product` への結合は **ほぼ O(N)** でスキャン可能\n",
136+
"\n",
137+
"---\n",
138+
"\n",
139+
"## 5) 計算量(概算)\n",
140+
"\n",
141+
"* `Sales` の件数を `N`、`Product` の件数を `M` とする\n",
142+
"* インデックス付きネストループ / ハッシュ結合を想定\n",
143+
"\n",
144+
"1. **JOIN 処理**\n",
145+
"\n",
146+
" * `Sales` を 1 度スキャン: O(N)\n",
147+
" * 各 `product_id` に対し `Product` をインデックスルックアップ: O(1) 近似 × N\n",
148+
" * 合計: **O(N)** 近似(`M` がメモリに乗る前提)\n",
149+
"\n",
150+
"2. **追加のソートや集計なし**\n",
151+
"\n",
152+
" * `GROUP BY` / `ORDER BY` がないため、\n",
153+
" ウィンドウ処理やソート起因の **O(N log N)** コストは発生しない\n",
154+
"\n",
155+
"---\n",
156+
"\n",
157+
"## 6) 図解(Mermaid 超保守版)\n",
158+
"\n",
159+
"```mermaid\n",
160+
"flowchart TD\n",
161+
" A[入力 Sales テーブル]\n",
162+
" B[入力 Product テーブル]\n",
163+
" C[結合 product_id で内部結合]\n",
164+
" D[列選択 product_name year price]\n",
165+
" E[出力 結果テーブル]\n",
166+
"\n",
167+
" A --> C\n",
168+
" B --> C\n",
169+
" C --> D\n",
170+
" D --> E\n",
171+
"```\n",
172+
"\n",
173+
"この問題は「JOIN の基本」を確認するタイプなので、\n",
174+
"**余計なウィンドウ関数や CTE を足さず、最短の INNER JOIN で書けるか**がポイントになります。\n",
175+
"\n",
176+
"結論から言うと、この問題に関しては **ほぼ最適** なので、実務目線でも LeetCode 目線でも「これ以上いじる価値がある改善」はありません 👍\n",
177+
"\n",
178+
"---\n",
179+
"\n",
180+
"## 1. 現在のクエリの評価\n",
181+
"\n",
182+
"```sql\n",
183+
"SELECT\n",
184+
" p.product_name,\n",
185+
" s.year,\n",
186+
" s.price\n",
187+
"FROM Sales AS s\n",
188+
"JOIN Product AS p\n",
189+
" ON p.product_id = s.product_id;\n",
190+
"```\n",
191+
"\n",
192+
"* やりたいこと:\n",
193+
"\n",
194+
" * `Sales` の各行に対して、`Product` から `product_name` を引いてくるだけ\n",
195+
"* それに対して:\n",
196+
"\n",
197+
" * 不要な列は出していない\n",
198+
" * 不要な `DISTINCT` / `GROUP BY` / `ORDER BY` もなし\n",
199+
" * 結合条件も PK–FK で素直\n",
200+
"\n",
201+
"**アルゴリズム的には O(N)**(`Sales` を 1 回なめて `Product` にインデックスルックアップ)なので、これ以上いい形はほぼありません。\n",
202+
"\n",
203+
"LeetCode の **Runtime 1155 ms / Beats 69.04%** も、この種の SQL 問題としては十分優秀な部類です。\n",
204+
"\n",
205+
"---\n",
206+
"\n",
207+
"## 2. 相関サブクエリ版が遅い理由\n",
208+
"\n",
209+
"```sql\n",
210+
"SELECT\n",
211+
" (\n",
212+
" SELECT p.product_name\n",
213+
" FROM Product AS p\n",
214+
" WHERE p.product_id = s.product_id\n",
215+
" ) AS product_name,\n",
216+
" s.year,\n",
217+
" s.price\n",
218+
"FROM Sales AS s;\n",
219+
"```\n",
220+
"\n",
221+
"* これは「`Sales` の行ごとに `Product` を探しに行く」構造なので、\n",
222+
"\n",
223+
" * プランによっては **行数 × サブクエリ** になりやすく、\n",
224+
" * JOIN よりもオーバーヘッドが大きい\n",
225+
"* その結果:\n",
226+
"\n",
227+
" * Runtime 5589 ms / Beats 5.01% と、数字が正直に「非効率です」と言ってます。\n",
228+
"\n",
229+
"**教科書的にも・実務的にも JOIN が正解**なので、ここは今の理解でバッチリです。\n",
230+
"\n",
231+
"---\n",
232+
"\n",
233+
"## 3. 「それでも何かできる?」という観点でのコメント\n",
234+
"\n",
235+
"LeetCode 環境だと、ここから先は **ほぼ誤差の世界** です。\n",
236+
"\n",
237+
"やれるとしてもせいぜい:\n",
238+
"\n",
239+
"* `JOIN` → `INNER JOIN` と明示してもプランはほぼ変わらないはず\n",
240+
"* `USING (product_id)` を使って少しだけスッキリ書く程度(性能は同じ)\n",
241+
"\n",
242+
"例:\n",
243+
"\n",
244+
"```sql\n",
245+
"SELECT\n",
246+
" p.product_name,\n",
247+
" s.year,\n",
248+
" s.price\n",
249+
"FROM Sales AS s\n",
250+
"JOIN Product AS p USING (product_id);\n",
251+
"\n",
252+
"Runtime 1306 ms\n",
253+
"Beats 31.05%\n",
254+
"\n",
255+
"```\n",
256+
"\n",
257+
"ですが、これは **可読性の好みの問題** であって、実行計画レベルでの改善はほとんど期待できません。\n",
258+
"\n",
259+
"---\n",
260+
"\n",
261+
"## 4. Runtime の数字をどう捉えるか\n",
262+
"\n",
263+
"LeetCode の Runtime は、以下のような要因でブレます:\n",
264+
"\n",
265+
"* 実行サーバの負荷状況\n",
266+
"* キャッシュの有無\n",
267+
"* 同じクエリでも再実行で数百 ms レベルで揺れることがある\n",
268+
"\n",
269+
"この問題のように\n",
270+
"\n",
271+
"* シンプルな JOIN\n",
272+
"* 行数もそこまで多くない\n",
273+
"* インデックス前提の PK–FK 結合\n",
274+
"\n",
275+
"というケースでは、**「Beats 69%」なら十分・これ以上追うのは趣味の最適化**という感覚で大丈夫です。\n",
276+
"\n",
277+
"---\n",
278+
"\n",
279+
"## まとめ\n",
280+
"\n",
281+
"* 今の JOIN クエリは **ロジックも計算量もベストプラクティスど真ん中**。\n",
282+
"* 相関サブクエリ版が遅いのも理屈どおりで、理解は完璧。\n",
283+
"* LeetCode Runtime はノイズも多いので、これ以上は「数字のための微調整」になりがちです。\n",
284+
"\n",
285+
"この問題については、**「もう終わりでよいレベル」**と判断して次の問題に進んでOKだと思います。\n",
286+
"\n"
287+
]
288+
},
289+
{
290+
"cell_type": "markdown",
291+
"id": "575a4473",
292+
"metadata": {},
293+
"source": []
294+
}
295+
],
296+
"metadata": {
297+
"language_info": {
298+
"name": "python"
299+
}
300+
},
301+
"nbformat": 4,
302+
"nbformat_minor": 5
303+
}

0 commit comments

Comments
 (0)