diff --git a/SQL/Leetcode/Basic join/1084. Sales Analysis III/gpt 5.1 thinking/Sales_Analysis_III_pandas.ipynb b/SQL/Leetcode/Basic join/1084. Sales Analysis III/gpt 5.1 thinking/Sales_Analysis_III_pandas.ipynb new file mode 100644 index 00000000..2417ad75 --- /dev/null +++ b/SQL/Leetcode/Basic join/1084. Sales Analysis III/gpt 5.1 thinking/Sales_Analysis_III_pandas.ipynb @@ -0,0 +1,292 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "50632a29", + "metadata": {}, + "source": [ + "## 0) 前提\n", + "\n", + "* 環境: **Python 3.10.15 / pandas 2.2.2**\n", + "* **指定シグネチャ厳守**\n", + "* I/O 禁止、`print` / `sort_values` 使用禁止\n", + "* 判定は `product_id` 基準、出力列・順序は `['product_id', 'product_name']`\n", + "\n", + "---\n", + "\n", + "## 1) 問題\n", + "\n", + "* `{{PROBLEM_STATEMENT}}`\n", + " 2019-01-01〜2019-03-31(2019 年 Q1)の期間にのみ販売された商品を求める。\n", + " 具体的には、各 `product_id` について:\n", + "\n", + " * Q1 期間に 1 回以上販売されている\n", + " * かつ Q1 以外の期間では 1 回も販売されていない\n", + "\n", + " ものを抽出する。\n", + "\n", + "* 入力 DF: `{{INPUT_DATAFRAMES}}`\n", + "\n", + " ```text\n", + " Product : 列 ['product_id', 'product_name', 'unit_price']\n", + " Sales : 列 ['seller_id', 'product_id', 'buyer_id', 'sale_date', 'quantity', 'price']\n", + " ```\n", + "\n", + "* 出力: `{{OUTPUT_COLUMNS_AND_RULES}}`\n", + "\n", + " * 列: `['product_id', 'product_name']`\n", + " * 行: 上記条件を満たす `product_id` のみ\n", + "\n", + "---\n", + "\n", + "## 2) 実装(指定シグネチャ厳守)\n", + "\n", + "> 列最小化 → `groupby` で min/max 日付集約 → 条件抽出 → `isin` でセミジョイン、という素直なパターンで実装します。\n", + "\n", + "```python\n", + "import pandas as pd\n", + "\n", + "def sales_analysis(product: pd.DataFrame, sales: pd.DataFrame) -> pd.DataFrame:\n", + " \"\"\"\n", + " Returns:\n", + " pd.DataFrame: 列名と順序は ['product_id', 'product_name']\n", + " \"\"\"\n", + " # 1) 列最小化(この問題で必要なのは product_id と sale_date のみ)\n", + " s = sales[['product_id', 'sale_date']]\n", + "\n", + " # 2) product_id ごとに最小日付・最大日付を集約\n", + " agg = (\n", + " s.groupby('product_id', as_index=False)['sale_date']\n", + " .agg(min_date='min', max_date='max')\n", + " )\n", + "\n", + " # 3) 「すべての販売日が 2019Q1 に収まっている」product_id を抽出\n", + " q1_start = pd.Timestamp('2019-01-01')\n", + " q1_end = pd.Timestamp('2019-03-31')\n", + "\n", + " mask = (agg['min_date'] >= q1_start) & (agg['max_date'] <= q1_end)\n", + " q1_only_ids = agg.loc[mask, 'product_id']\n", + "\n", + " # 4) Product から該当 product_id のみセミジョイン\n", + " out = product.loc[product['product_id'].isin(q1_only_ids), ['product_id', 'product_name']]\n", + "\n", + " return out\n", + "\n", + "Analyze Complexity\n", + "Runtime 351 ms\n", + "Beats 79.45%\n", + "Memory 69.55 MB\n", + "Beats 81.99%\n", + "\n", + "```\n", + "\n", + "* `sort_values` は一切使用せず、`groupby` 集約と `isin` のみで完結させています。\n", + "* 出力列の順序も指定どおり `['product_id', 'product_name']` に固定しています。\n", + "\n", + "---\n", + "\n", + "## 3) アルゴリズム説明\n", + "\n", + "使用 API と流れを整理します。\n", + "\n", + "1. **列最小化**:\n", + "\n", + " ```python\n", + " s = sales[['product_id', 'sale_date']]\n", + " ```\n", + "\n", + " * 後続処理に不要な列(seller_id, buyer_id, quantity, price)はここで捨ててメモリと処理コストを削減。\n", + "\n", + "2. **グループ処理(最小/最大日付の集約)**:\n", + "\n", + " ```python\n", + " agg = (\n", + " s.groupby('product_id', as_index=False)['sale_date']\n", + " .agg(min_date='min', max_date='max')\n", + " )\n", + " ```\n", + "\n", + " * 各 `product_id` について\n", + "\n", + " * `min_date`: その商品の最も古い販売日\n", + " * `max_date`: その商品の最も新しい販売日\n", + " * 「すべての販売日が Q1 にある」ことは\n", + " `min_date >= '2019-01-01'` かつ `max_date <= '2019-03-31'`\n", + " と同値になるので、`BOOL_OR` などのフラグ集計よりもシンプルです。\n", + "\n", + "3. **条件抽出**:\n", + "\n", + " ```python\n", + " q1_start = pd.Timestamp('2019-01-01')\n", + " q1_end = pd.Timestamp('2019-03-31')\n", + "\n", + " mask = (agg['min_date'] >= q1_start) & (agg['max_date'] <= q1_end)\n", + " q1_only_ids = agg.loc[mask, 'product_id']\n", + " ```\n", + "\n", + " * `mask` で「Q1 だけで売れている」product_id を絞り込み、その ID シリーズを取得。\n", + "\n", + "4. **軽量セミジョイン (`isin`)**:\n", + "\n", + " ```python\n", + " out = product.loc[product['product_id'].isin(q1_only_ids),\n", + " ['product_id', 'product_name']]\n", + " ```\n", + "\n", + " * 単一キー → 行のフィルタには `merge` よりも `isin` が軽くて読みやすいパターン。\n", + " * ここで Product 側から必要な列だけを投影して最終結果にしている。\n", + "\n", + "---\n", + "\n", + "### NULL / 重複 / 型の扱い\n", + "\n", + "* `sale_date` が NULL の行があった場合\n", + "\n", + " * `min` / `max` はデフォルトで非 NULL の値だけを対象にするため、NULL の存在で壊れにくい設計。\n", + " * 全て NULL の場合は `min_date`, `max_date` が NULL となり、Q1 条件に該当しなくなる(妥当)。\n", + "\n", + "* 重複行(同じ `product_id`・`sale_date` の行が複数)\n", + "\n", + " * `groupby` 集約では重複があっても `min_date` / `max_date` の値は変わらないため、追加コストは O(重複数) だけでロジックは影響なし。\n", + "\n", + "* 型\n", + "\n", + " * LeetCode 想定では `sale_date` はすでに `datetime64[ns]` 相当の型で渡される想定。\n", + " * 明示的に `pd.Timestamp` を使って比較することで、文字列比較ではなく日時比較で安全に評価している。\n", + "\n", + "---\n", + "\n", + "## 4) 計算量(概算)\n", + "\n", + "`N = Sales の行数`, `P = Product の行数`, `G = 異なる product_id の個数` とします。\n", + "\n", + "1. `groupby('product_id').agg(min, max)`\n", + "\n", + " * ハッシュグループ化想定で **O(N)**〜**O(N log G)** 近辺\n", + " * `sale_date` の min/max 計算は 1 行あたり O(1)\n", + "\n", + "2. `isin` によるセミジョイン\n", + "\n", + " * `q1_only_ids` の長さを `K` とすると\n", + "\n", + " * ハッシュセット化で **O(K)**\n", + " * Product 側の走査で **O(P)**\n", + " * 合計は **O(P + K)** 程度(`K <= G <= N`)\n", + "\n", + "トータルで **O(N + P)** 近似の線形オーダーに収まり、\n", + "`sort_values` や複雑な rank を使わない分、実装もパフォーマンスもバランスが良い構成です。\n", + "\n", + "---\n", + "\n", + "## 5) 図解(Mermaid 超保守版)\n", + "\n", + "```mermaid\n", + "flowchart TD\n", + " A[入力 Product DF]\n", + " B[入力 Sales DF]\n", + " C[Sales から product_id と sale_date のみ抽出]\n", + " D[product_id ごとに min と max の sale_date を集約]\n", + " E[集約結果から 2019Q1 だけで売れている product_id を抽出]\n", + " F[Product DF から該当 product_id のみフィルタ]\n", + " G[出力 product_id と product_name]\n", + " B --> C\n", + " C --> D\n", + " D --> E\n", + " A --> F\n", + " E --> F\n", + " F --> G\n", + "```\n", + "\n", + "この実装は、PostgreSQL 版で行った「min/max 日付で Q1 に収まるか判定」という戦略を、そのまま Pandas 向けに最適化した形になっています。\n", + "\n", + "結論から言うと、そのスコアならかなり仕上がっています。\n", + "アルゴリズム自体はほぼ最適クラスなので、残りは「微妙なオーバーヘッドを減らすかどうか」の世界です。\n", + "\n", + "それでもあえていじるとしたら、このくらいです。\n", + "\n", + "---\n", + "\n", + "## 1) ほぼ最終形のコード例\n", + "\n", + "* 中間の `s` をやめて `groupby` を直にかける\n", + "* マスク計算時に `Series` ではなく `ndarray` を使って、インデックスアラインのオーバーヘッドを削る\n", + "\n", + "```python\n", + "import pandas as pd\n", + "\n", + "def sales_analysis(product: pd.DataFrame, sales: pd.DataFrame) -> pd.DataFrame:\n", + " \"\"\"\n", + " Returns:\n", + " pd.DataFrame: 列名と順序は ['product_id', 'product_name']\n", + " \"\"\"\n", + " # 1) product_id ごとに最小日付・最大日付を集約(列も最小限)\n", + " agg = (\n", + " sales.groupby('product_id', as_index=False)['sale_date']\n", + " .agg(min_date='min', max_date='max')\n", + " )\n", + "\n", + " # 2) 「すべての販売日が 2019Q1 に収まっている」product_id を抽出\n", + " q1_start = pd.Timestamp('2019-01-01')\n", + " q1_end = pd.Timestamp('2019-03-31')\n", + "\n", + " min_date = agg['min_date'].values\n", + " max_date = agg['max_date'].values\n", + " mask = (min_date >= q1_start) & (max_date <= q1_end)\n", + "\n", + " q1_only_ids = agg.loc[mask, 'product_id']\n", + "\n", + " # 3) Product から該当 product_id のみセミジョイン\n", + " out = product.loc[\n", + " product['product_id'].isin(q1_only_ids),\n", + " ['product_id', 'product_name']\n", + " ]\n", + "\n", + " return out\n", + "\n", + "Analyze Complexity\n", + "Runtime 331 ms\n", + "Beats 94.28%\n", + "Memory 69.50 MB\n", + "Beats 81.99%\n", + "\n", + "```\n", + "\n", + "### 微調整ポイント\n", + "\n", + "* `sales[['product_id', 'sale_date']]` という別変数を作らず、そのまま groupby している\n", + " → ほんの少しだけ一時オブジェクトが減ります(効果はごく小さいですが無駄はない)。\n", + "* `mask` 計算を `Series` 同士ではなく `ndarray` 同士にしている\n", + " → インデックス合わせなどの内部処理がいらなくなる分、わずかに軽くなり得ます。\n", + "\n", + "---\n", + "\n", + "## 2) これ以上やるなら「可読性とのトレードオフ」\n", + "\n", + "アルゴリズムは\n", + "\n", + "* 1 回の `groupby` で min/max を取る\n", + "* `isin` で Product とセミジョイン\n", + "\n", + "という構造で、計算量的にも実質 O(N) に近い形です。\n", + "\n", + "これ以上の改善は\n", + "\n", + "* dtype を事前に `datetime64[ns]` に統一しておく\n", + "* 変数名・ローカル変数の数をさらに削る\n", + "\n", + "といったレベルになり、可読性とトレードオフになる割に LeetCode の ms 単位ではほぼ誤差です。\n", + "\n", + "今の 351 ms / 79%・メモリ 69.55MB / 81% なら、\n", + "「アルゴリズムも実装も合格点、余力があれば micro-tuning で遊べる状態」と見てよいと思います。\n", + "\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/SQL/Leetcode/Basic join/1084. Sales Analysis III/gpt 5.1 thinking/Sales_Analysis_III_postgre.ipynb b/SQL/Leetcode/Basic join/1084. Sales Analysis III/gpt 5.1 thinking/Sales_Analysis_III_postgre.ipynb new file mode 100644 index 00000000..bc203300 --- /dev/null +++ b/SQL/Leetcode/Basic join/1084. Sales Analysis III/gpt 5.1 thinking/Sales_Analysis_III_postgre.ipynb @@ -0,0 +1,399 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1e66ef32", + "metadata": {}, + "source": [ + "## 0) 前提\n", + "\n", + "* エンジン: **PostgreSQL 16.6+**\n", + "* 並び順: 任意(`ORDER BY` 不要)\n", + "* `NOT IN` は使用せず、`EXISTS` / `NOT EXISTS` / `LEFT JOIN ... IS NULL` を推奨\n", + "* 判定は `product_id` 基準、表示は仕様どおり `product_id, product_name`\n", + "\n", + "---\n", + "\n", + "## 1) 問題\n", + "\n", + "* `{{PROBLEM_STATEMENT}}`\n", + " 2019-01-01 〜 2019-03-31(2019年Q1)にのみ販売された商品を求める。\n", + " すなわち、\n", + "\n", + " * 2019-01-01〜2019-03-31 の間に少なくとも 1 回は売れている\n", + " * それ以外の日付には 1 回も売れていない\n", + " という条件を満たす `product_id` を抽出する。\n", + "\n", + "* 入力: `{{TABLES_OR_SCHEMAS}}`\n", + "\n", + " ```text\n", + " Product(\n", + " product_id int PK,\n", + " product_name varchar,\n", + " unit_price int\n", + " )\n", + "\n", + " Sales(\n", + " seller_id int,\n", + " product_id int FK -> Product.product_id,\n", + " buyer_id int,\n", + " sale_date date,\n", + " quantity int,\n", + " price int\n", + " )\n", + " ```\n", + "\n", + "* 出力: `{{OUTPUT_COLUMNS_AND_RULES}}`\n", + "\n", + " * 列: `product_id, product_name`\n", + " * 対象行:\n", + "\n", + " * Sales において、その `product_id` が\n", + "\n", + " * `sale_date BETWEEN '2019-01-01' AND '2019-03-31'` を満たす行を少なくとも 1 つ持ち\n", + " * かつ、それ以外の sale_date を持たない\n", + "\n", + "---\n", + "\n", + "## 2) 最適解(単一クエリ)\n", + "\n", + "PostgreSQL なので、ブール集計関数 `BOOL_OR` をウィンドウ関数として使うとかなり素直に書けます。\n", + "\n", + "```sql\n", + "WITH pre AS (\n", + " SELECT\n", + " s.product_id,\n", + " s.sale_date\n", + " FROM Sales AS s\n", + "),\n", + "win AS (\n", + " SELECT\n", + " product_id,\n", + " BOOL_OR(sale_date BETWEEN DATE '2019-01-01' AND DATE '2019-03-31')\n", + " OVER (PARTITION BY product_id) AS sold_in_q1,\n", + " BOOL_OR(\n", + " sale_date < DATE '2019-01-01'\n", + " OR sale_date > DATE '2019-03-31'\n", + " ) OVER (PARTITION BY product_id) AS sold_outside_q1\n", + " FROM pre\n", + ")\n", + "SELECT DISTINCT\n", + " p.product_id,\n", + " p.product_name\n", + "FROM Product AS p\n", + "JOIN win AS w\n", + " ON p.product_id = w.product_id\n", + "WHERE w.sold_in_q1 -- Q1 で一度は売れている\n", + " AND NOT w.sold_outside_q1; -- Q1 以外では一度も売れていない\n", + "\n", + "Runtime 870 ms\n", + "Beats 79.32%\n", + "\n", + "```\n", + "\n", + "### この形のポイント\n", + "\n", + "* `pre`\n", + "\n", + " * 今回は `Sales` から必要な列(`product_id, sale_date`)だけ抜き出す軽い前処理。\n", + " * 実際には `pre` を省略して `FROM Sales` から直接 `win` に入れても問題ありません。\n", + "\n", + "* `win`\n", + "\n", + " * `BOOL_OR(condition) OVER (PARTITION BY product_id)` で\n", + "\n", + " * その商品が Q1 で一度でも売れていれば `sold_in_q1 = true`\n", + " * それ以外の期間で一度でも売れていれば `sold_outside_q1 = true`\n", + " * Sales に重複行があっても、「一度でも売れていれば true」という要件なのでブール OR で自然に吸収できます。\n", + "\n", + "* 最終 SELECT\n", + "\n", + " * Product と結合し、仕様どおり `product_id, product_name` を取得。\n", + " * `Sales` 由来で複数行に膨らむ可能性があるので `DISTINCT` で 1 行に整理。\n", + "\n", + "---\n", + "\n", + "### 代替(`EXISTS` / `NOT EXISTS` ベース)\n", + "\n", + "LATERAL を使う必要はないシンプルな問題なので、読みやすさ重視ならこの形も有力です。\n", + "\n", + "```sql\n", + "SELECT\n", + " p.product_id,\n", + " p.product_name\n", + "FROM Product AS p\n", + "WHERE EXISTS (\n", + " -- Q1 期間に少なくとも 1 回売れている\n", + " SELECT 1\n", + " FROM Sales AS s\n", + " WHERE s.product_id = p.product_id\n", + " AND s.sale_date BETWEEN DATE '2019-01-01' AND DATE '2019-03-31'\n", + ")\n", + "AND NOT EXISTS (\n", + " -- Q1 以外の期間に 1 回も売れていないことを確認\n", + " SELECT 1\n", + " FROM Sales AS s\n", + " WHERE s.product_id = p.product_id\n", + " AND (\n", + " s.sale_date < DATE '2019-01-01'\n", + " OR s.sale_date > DATE '2019-03-31'\n", + " )\n", + ");\n", + "\n", + "Runtime 906 ms\n", + "Beats 63.06%\n", + "\n", + "```\n", + "\n", + "* `NOT IN` ではなく `NOT EXISTS` を利用しており、NULL 罠を回避しています。\n", + "* インデックス例: `CREATE INDEX ON Sales (product_id, sale_date);`\n", + " これがあると、両方のサブクエリが効率的に走ります。\n", + "\n", + "---\n", + "\n", + "## 3) 要点解説\n", + "\n", + "### ウィンドウ版(最適解)の設計意図\n", + "\n", + "1. **product_id 単位のフラグ判定に集約する**\n", + "\n", + " * 「Q1 で売れたか」\n", + " `BOOL_OR(sale_date BETWEEN '2019-01-01' AND '2019-03-31')`\n", + " * 「Q1 以外で売れたか」\n", + " `BOOL_OR(sale_date < '2019-01-01' OR sale_date > '2019-03-31')`\n", + "\n", + " を `PARTITION BY product_id` でウィンドウ集計してしまえば、\n", + " あとは `sold_in_q1 = true AND sold_outside_q1 = false` でフィルタするだけになります。\n", + "\n", + "2. **前処理 CTE `pre` で列を絞る**\n", + "\n", + " * 実務では、`SELECT product_id, sale_date FROM Sales` のように、\n", + " ウィンドウに本当に必要な列だけを流すと\n", + "\n", + " * ネットワーク転送量削減\n", + " * 一時領域の節約\n", + " に効きます。\n", + " * PostgreSQL のプランナーはかなり賢いですが、明示的に列を減らしておく方が安定して良いプランが出やすいケースもあります。\n", + "\n", + "3. **PostgreSQL らしい書き方**\n", + "\n", + " * MySQL だと `MAX(CASE WHEN ... THEN 1 ELSE 0 END)` になりがちなところを、\n", + " PostgreSQL では `BOOL_OR(condition)` で自然言語に近い書き方にできます。\n", + " * 集約関数をそのまま `OVER (...)` に乗せることで「ウィンドウ集約」として利用。\n", + "\n", + "### LATERAL の話(今回は不要)\n", + "\n", + "* テンプレートにあるような LATERAL JOIN は、\n", + " 「各 product_id から sale_date が早い方から 1 件だけ」など\n", + " 「グループごとに上位 k 件だけ取る」場面で威力を発揮します。\n", + "* 今回は「全期間をざっくり Q1 / Q1 以外に二分してブール判定」するだけなので、LATERAL はオーバーキルです。\n", + "\n", + "### NULL / 重複の扱い\n", + "\n", + "* `sale_date` が NULL の行があっても\n", + "\n", + " * `sale_date BETWEEN ...` は false\n", + " * `<` や `>` も `NULL` なら false\n", + " なので、`BOOL_OR` の対象から自然に外れます(影響なし)。\n", + "* 重複行(同じ product_id, sale_date が複数行)も、\n", + " 「一度でも売れていればよい」という仕様なので `BOOL_OR` には影響しません。\n", + "\n", + "---\n", + "\n", + "## 4) 計算量(概算)\n", + "\n", + "### ウィンドウ版(最適解)\n", + "\n", + "* `Sales` の件数を `N`、異なる `product_id` 数を `G` とすると:\n", + "\n", + " * `pre`: 単純スキャンで **O(N)**\n", + " * `win` ウィンドウ処理:\n", + "\n", + " * `PARTITION BY product_id` でパーティションを作り、各パーティション内で `BOOL_OR` を計算\n", + " * プラン次第ですが、典型的にはパーティションごとのソートを含み **O(Σ n_g log n_g)**(n_g は各グループサイズ)\n", + " * `Product` との結合:\n", + "\n", + " * `Product.product_id` が PK なので、Hash Join / Merge Join で **O(G)** 近似\n", + "\n", + "### EXISTS / NOT EXISTS 版\n", + "\n", + "* `Product` の件数を `M` とすると:\n", + "\n", + " * 各 Product 行について\n", + "\n", + " * `EXISTS` で Q1 行の存在確認\n", + " * `NOT EXISTS` で Q1 以外行の不在確認\n", + " * `Sales(product_id, sale_date)` にインデックスがあれば、どちらも Index Scan で **O(log N)** 近辺\n", + " * 全体でおおよそ **O(M log N)** 程度\n", + "\n", + "PostgreSQL ではどちらの書き方でもよいプランを出してくれることが多く、\n", + "テーブルサイズや既存インデックス構成に応じて選択すれば十分です。\n", + "\n", + "---\n", + "\n", + "## 5) 図解(Mermaid 超保守版)\n", + "\n", + "```mermaid\n", + "flowchart TD\n", + " A[入力 Product テーブル]\n", + " B[入力 Sales テーブル]\n", + " C[Sales を product_id ごとに Q1 と Q1 以外の販売有無を判定]\n", + " D[Q1 に販売あり かつ Q1 以外に販売なし の商品を抽出]\n", + " E[Product と結合して商品名を取得]\n", + " F[出力 product_id と product_name]\n", + " B --> C\n", + " C --> D\n", + " A --> E\n", + " D --> E\n", + " E --> F\n", + "```\n", + "\n", + "かなり良いスコアですが、まだチューニングの余地はあります。\n", + "ポイントは「早くグループ化して行数を減らす」「ウィンドウ関数をやめて通常の集約に寄せる」です。\n", + "\n", + "---\n", + "\n", + "## 1. 一番おすすめ:`MIN` / `MAX` 集約でシンプルに判定\n", + "\n", + "この問題は本質的に:\n", + "\n", + "* その product の **最小日付が Q1 以上**\n", + "* かつ **最大日付が Q1 以下**\n", + "\n", + "であれば、「その product の販売日はすべて Q1 に含まれている」ことになります。\n", + "\n", + "なので、`Sales` を `product_id` で一度 `GROUP BY` してしまう設計が一番素直で速いです。\n", + "\n", + "```sql\n", + "WITH agg AS (\n", + " SELECT\n", + " s.product_id,\n", + " MIN(s.sale_date) AS min_date,\n", + " MAX(s.sale_date) AS max_date\n", + " FROM Sales AS s\n", + " GROUP BY s.product_id\n", + ")\n", + "SELECT\n", + " p.product_id,\n", + " p.product_name\n", + "FROM Product AS p\n", + "JOIN agg AS a\n", + " ON p.product_id = a.product_id\n", + "WHERE a.min_date >= DATE '2019-01-01'\n", + " AND a.max_date <= DATE '2019-03-31';\n", + "\n", + "Runtime 946 ms\n", + "Beats 47.83%\n", + "\n", + "```\n", + "\n", + "### なぜ速くなりやすいか\n", + "\n", + "* ウィンドウ関数や `BOOL_OR OVER (...)` をやめて、**1 回の GROUP BY 集約**に落としている\n", + "* `Sales` は最初から `GROUP BY product_id` で**行数を product の種類数まで圧縮**できる\n", + "\n", + " * その後の JOIN / WHERE は「product_id 行数」単位で処理\n", + "* 外側で `DISTINCT` も不要なので、余計なソートや Hash Aggregate を避けられる\n", + "\n", + "LeetCode 環境でも、このパターンはかなり安定して速い部類です。\n", + "\n", + "---\n", + "\n", + "## 2. ロジック維持しつつ高速化:`BOOL_OR` + `GROUP BY` 版\n", + "\n", + "いまのクエリは「行ごとにウィンドウで `BOOL_OR`」を計算しているので、同じ `product_id` の行で同じフラグが何度も重複して出てきます。\n", + "これを「ウィンドウ」ではなく、「通常の集約」に変えるだけでもかなりスリムになります。\n", + "\n", + "```sql\n", + "WITH flags AS (\n", + " SELECT\n", + " s.product_id,\n", + " BOOL_OR(s.sale_date BETWEEN DATE '2019-01-01' AND DATE '2019-03-31')\n", + " AS sold_in_q1,\n", + " BOOL_OR(\n", + " s.sale_date < DATE '2019-01-01'\n", + " OR s.sale_date > DATE '2019-03-31'\n", + " ) AS sold_outside_q1\n", + " FROM Sales AS s\n", + " GROUP BY s.product_id\n", + ")\n", + "SELECT\n", + " p.product_id,\n", + " p.product_name\n", + "FROM Product AS p\n", + "JOIN flags AS f\n", + " ON p.product_id = f.product_id\n", + "WHERE f.sold_in_q1\n", + " AND NOT f.sold_outside_q1;\n", + "\n", + "Runtime 958 ms\n", + "Beats 43.58%\n", + "\n", + "```\n", + "\n", + "### 元クエリとの差分\n", + "\n", + "* `OVER (PARTITION BY product_id)` をやめて `GROUP BY product_id` に変更\n", + "* その結果、`flags` は product_id ごとに **1 行** だけになり、\n", + "\n", + " * 外側の `SELECT DISTINCT` が不要\n", + " * 結果セットのサイズが小さくなり、その後の処理が軽くなる\n", + "\n", + "ロジック自体は元のウィンドウ版と等価なので、読みやすさを維持しつつパフォーマンスだけ改善しやすい案です。\n", + "\n", + "---\n", + "\n", + "## 3. `EXISTS / NOT EXISTS` 版の位置づけ\n", + "\n", + "あなたの 2 本目のクエリ:\n", + "\n", + "```sql\n", + "SELECT\n", + " p.product_id,\n", + " p.product_name\n", + "FROM Product AS p\n", + "WHERE EXISTS ( ...Q1... )\n", + "AND NOT EXISTS ( ...Q1 以外... );\n", + "```\n", + "\n", + "も設計としては非常に良いです。\n", + "\n", + "* LeetCode の Postgres 環境はインデックスが無いことが多く、\n", + " そこで `EXISTS` / `NOT EXISTS` がウィンドウ版より遅く出るのは十分あり得ます。\n", + "* 実務の Postgres + インデックスあり環境だと、\n", + " `Sales(product_id, sale_date)` にインデックスがあればかなり強いパターンです。\n", + "\n", + "LeetCode のスコアをもうひと伸ばししたいなら、`GROUP BY` で一気に集約してしまう ① or ② のパターンの方が有利なことが多い、くらいの理解でよいと思います。\n", + "\n", + "---\n", + "\n", + "## 4. まとめ(改善ポイントの要約)\n", + "\n", + "* 改善余地は「**ウィンドウ関数 → 通常の集約に変更**」「**外側の DISTINCT を消す**」の 2 点\n", + "* ベスト候補は:\n", + "\n", + " ```sql\n", + " WITH agg AS (\n", + " SELECT product_id, MIN(sale_date) AS min_date, MAX(sale_date) AS max_date\n", + " FROM Sales\n", + " GROUP BY product_id\n", + " )\n", + " SELECT p.product_id, p.product_name\n", + " FROM Product p\n", + " JOIN agg a ON p.product_id = a.product_id\n", + " WHERE a.min_date >= DATE '2019-01-01'\n", + " AND a.max_date <= DATE '2019-03-31';\n", + " ```\n", + "\n", + "この形に差し替えて一度 LeetCode の Runtime / Beats を見てみると、たぶんもう一段階上のパーセンタイルを狙えると思います。\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}