From 0876463f0505065f317eabc2f1fbf345d83dbb37 Mon Sep 17 00:00:00 2001 From: myoshizumi Date: Wed, 29 Oct 2025 22:02:39 +0900 Subject: [PATCH] SQL: Advanced Select 601. Human Traffic of Stadium --- .gitignore | 105 ++++++ .python-version | 2 +- .../gpt/Human_Traffic_of_Stadium_mysql.ipynb | 319 ++++++++++++++++++ .../gpt/Human_Traffic_of_Stadium_pandas.ipynb | 223 ++++++++++++ .../Human_Traffic_of_Stadium_posgres.ipynb | 271 +++++++++++++++ prettier.config.js | 13 - 6 files changed, 919 insertions(+), 14 deletions(-) create mode 100644 .gitignore create mode 100644 SQL/Leetcode/Advanced select/601. Human Traffic of Stadium/gpt/Human_Traffic_of_Stadium_mysql.ipynb create mode 100644 SQL/Leetcode/Advanced select/601. Human Traffic of Stadium/gpt/Human_Traffic_of_Stadium_pandas.ipynb create mode 100644 SQL/Leetcode/Advanced select/601. Human Traffic of Stadium/gpt/Human_Traffic_of_Stadium_posgres.ipynb delete mode 100644 prettier.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ffe38959 --- /dev/null +++ b/.gitignore @@ -0,0 +1,105 @@ +# ======================================== +# プロジェクト用 .gitignore +# ======================================== + +# バイトコード / キャッシュ +__pycache__/ +*.py[cod] +*$py.class +*.pyc +*.pyo +*.pyd + +# 仮想環境 +.env +.venv +.venv3.10.13 +env/ +venv/ +ENV/ +.Python + +# パッケージ管理 +*.egg +*.egg-info/ +dist/ +build/ +.eggs/ +wheels/ +pip-wheel-metadata/ +*.manifest +*.spec + +# ログ / 一時ファイル +*.log +*.pot +*.tmp +*.swp +*.swo + +# テスト関連 +.coverage +.tox/ +.nox/ +.pytest_cache/ +htmlcov/ +coverage.xml +*.cover +*.py,cover + +# Jupyter Notebook +.ipynb_checkpoints/ +*/.ipynb_checkpoints/* +*.nbconvert.ipynb + +# IDE / エディタ関連 +.vscode/ +.idea/ +*.sublime-project +*.sublime-workspace + +# OS依存ファイル +.DS_Store +Thumbs.db + +# MyPy / Pyre / Type checker +.mypy_cache/ +.dmypy.json +dmypy.json +.pyre/ +.pytype/ + +# その他キャッシュ +.cache/ +.pybuilder/ + +# データファイル(大容量のもの) +data/raw/ +*.csv +*.xlsx +*.parquet +*.h5 +*.hdf5 + +# 機械学習モデル +models/ +*.pkl +*.joblib +*.pb + +# 画像・動画ファイル +*.png +*.jpg +*.jpeg +*.gif +*.mp4 +*.avi + +# 環境ファイル +config.ini +credentials.json + +# Node.js関連 +node_modules/ +package-lock.json +bun.lock \ No newline at end of file diff --git a/.python-version b/.python-version index dd6a2206..25ce095a 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.12.4 \ No newline at end of file +3.12.11 \ No newline at end of file diff --git a/SQL/Leetcode/Advanced select/601. Human Traffic of Stadium/gpt/Human_Traffic_of_Stadium_mysql.ipynb b/SQL/Leetcode/Advanced select/601. Human Traffic of Stadium/gpt/Human_Traffic_of_Stadium_mysql.ipynb new file mode 100644 index 00000000..53121c5b --- /dev/null +++ b/SQL/Leetcode/Advanced select/601. Human Traffic of Stadium/gpt/Human_Traffic_of_Stadium_mysql.ipynb @@ -0,0 +1,319 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# MySQL 8.0.40\n", + "\n", + "## 0) 前提\n", + "\n", + "* エンジン: **MySQL 8**\n", + "* 並び順: 任意(`ORDER BY` を付けない)\n", + " ※本問題は仕様で **`visit_date` 昇順** を要求 → 最終行に `ORDER BY visit_date`\n", + "* `NOT IN` は NULL 罠のため回避\n", + "* 判定は **ID 基準**(連続 ID かつ各行 `people >= 100`)、表示は仕様どおりの列名と順序\n", + "\n", + "## 1) 問題\n", + "\n", + "* `3 つ以上の連続した id を持ち、各行の people >= 100 のレコードを表示する。結果は visit_date 昇順。`\n", + "* 入力テーブル例: `Stadium(id INT, visit_date DATE, people INT)`\n", + "* 出力仕様: `id, visit_date, people` を **連続 ID の島(gaps-and-islands)** のうち長さ ≥ 3 の行のみ。最終並びは `visit_date ASC`。\n", + "\n", + "## 2) 最適解(単一クエリ)\n", + "\n", + "> people ≥ 100 を先に絞り込み、`id - ROW_NUMBER()` で **連続 ID の島キー** を作り、長さ ≥ 3 の島だけ残す。\n", + "\n", + "```sql\n", + "WITH pre AS (\n", + " SELECT id, visit_date, people\n", + " FROM Stadium\n", + " WHERE people >= 100\n", + "),\n", + "grp AS (\n", + " SELECT\n", + " id,\n", + " visit_date,\n", + " people,\n", + " id - ROW_NUMBER() OVER (ORDER BY id) AS grp_key\n", + " FROM pre\n", + "),\n", + "big_islands AS (\n", + " SELECT grp_key\n", + " FROM grp\n", + " GROUP BY grp_key\n", + " HAVING COUNT(*) >= 3\n", + ")\n", + "SELECT\n", + " g.id,\n", + " g.visit_date,\n", + " g.people\n", + "FROM grp AS g\n", + "JOIN big_islands AS b\n", + " USING (grp_key)\n", + "ORDER BY g.visit_date;\n", + "\n", + "Runtime 352 ms\n", + "Beats 56.94%\n", + "\n", + "```\n", + "\n", + "## 3) 代替解\n", + "\n", + "> `LAG` を使って「連続しているか」をフラグ化し、累積和で島を採番する方法。\n", + "\n", + "```sql\n", + "WITH pre AS (\n", + " SELECT id, visit_date, people\n", + " FROM Stadium\n", + " WHERE people >= 100\n", + "),\n", + "marked AS (\n", + " SELECT\n", + " id,\n", + " visit_date,\n", + " people,\n", + " CASE WHEN id = LAG(id) OVER (ORDER BY id) + 1 THEN 0 ELSE 1 END AS is_break\n", + " FROM pre\n", + "),\n", + "islands AS (\n", + " SELECT\n", + " id,\n", + " visit_date,\n", + " people,\n", + " SUM(is_break) OVER (ORDER BY id) AS grp_key\n", + " FROM marked\n", + "),\n", + "big_islands AS (\n", + " SELECT grp_key\n", + " FROM islands\n", + " GROUP BY grp_key\n", + " HAVING COUNT(*) >= 3\n", + ")\n", + "SELECT i.id, i.visit_date, i.people\n", + "FROM islands AS i\n", + "JOIN big_islands AS b USING (grp_key)\n", + "ORDER BY i.visit_date;\n", + "\n", + "Runtime 335 ms\n", + "Beats 76.87%\n", + "\n", + "```\n", + "\n", + "## 4) 要点解説\n", + "\n", + "* **判定基準は ID の連続**:日付は連続でなくてよい(問題文のとおり)。\n", + "* **Gaps-and-Islands パターン**:`id - ROW_NUMBER()` が同じ値の集合は ID が連番の「島」になる。\n", + "* 先に `people >= 100` を絞ることでウィンドウ行数を縮小し、高速化。\n", + "* `NOT IN` 不使用。結合は `JOIN ... USING (grp_key)` を採用。\n", + "* 並び順は仕様に従い **`visit_date ASC`**。\n", + "\n", + "## 5) 計算量(概算)\n", + "\n", + "* フィルタ後レコード数を `n` とすると:\n", + "\n", + " * ウィンドウ関数(`ROW_NUMBER` / `LAG`): **O(n log n)**(`ORDER BY id`)\n", + " * `GROUP BY grp_key`: **O(n)**〜**O(n log n)**\n", + " * 結合: **O(n)** 近似\n", + "* インデックス推奨: `PRIMARY KEY(id)` / `INDEX(people)`(`people >= 100` の選択度が高いほど効く)\n", + "\n", + "## 6) 図解(Mermaid 超保守版)\n", + "\n", + "```mermaid\n", + "flowchart TD\n", + " A[入力 Stadium] --> B[前処理 people >= 100]\n", + " B --> C[ウィンドウ id - ROW_NUMBER で島キー]\n", + " C --> D[島ごとに COUNT>=3 を抽出]\n", + " D --> E[該当島と結合して投影]\n", + " E --> F[visit_date 昇順で出力]\n", + "```\n", + "\n", + "まだ少しだけ速く・シンプルにできます。主な改善点は **`big_islands` との結合をやめて、ウィンドウ `COUNT()` で島の長さを直接フィルタ**することと、**適切なインデックス**です。\n", + "\n", + "---\n", + "\n", + "## 改善版(JOIN 排除・1 回のスキャンで判定)\n", + "\n", + "```sql\n", + "WITH pre AS (\n", + " SELECT id, visit_date, people\n", + " FROM Stadium\n", + " WHERE people >= 100\n", + "),\n", + "grp AS (\n", + " SELECT\n", + " id,\n", + " visit_date,\n", + " people,\n", + " id - ROW_NUMBER() OVER (ORDER BY id) AS grp_key\n", + " FROM pre\n", + ")\n", + "SELECT id, visit_date, people\n", + "FROM (\n", + " SELECT\n", + " g.*,\n", + " COUNT(*) OVER (PARTITION BY grp_key) AS island_len\n", + " FROM grp AS g\n", + ") x\n", + "WHERE island_len >= 3\n", + "ORDER BY visit_date;\n", + "\n", + "Runtime 353 ms\n", + "Beats 55.35%\n", + "\n", + "```\n", + "\n", + "**ポイント**\n", + "\n", + "* `big_islands` と `JOIN` を削除 → マテリアライズや結合コストを削減\n", + "* 同一 `grp_key`(連番の島)内の行数を `COUNT(*) OVER (PARTITION BY grp_key)` で算出し、外側で `WHERE island_len >= 3`\n", + "* 可読性も向上\n", + "\n", + "実行計画上は「`ORDER BY id` のウィンドウ → `PARTITION BY grp_key` のウィンドウ → 最終フィルタ」の二段で済みます。\n", + "\n", + "---\n", + "\n", + "## 代替の等価書き換え(`ROW_NUMBER` を 1 回に)\n", + "\n", + "MySQL は同一 SELECT 句でエイリアスを別のウィンドウ関数の `PARTITION BY` に直接使えないため、上のように 2 段に分けます。もし 1 段に詰めたい場合は、CTE を 1 個にして派生表で包むのが最小です。\n", + "\n", + "```sql\n", + "SELECT id, visit_date, people\n", + "FROM (\n", + " SELECT\n", + " id,\n", + " visit_date,\n", + " people,\n", + " COUNT(*) OVER (PARTITION BY (id - ROW_NUMBER() OVER (ORDER BY id))) AS island_len\n", + " FROM Stadium\n", + " WHERE people >= 100\n", + ") t\n", + "WHERE island_len >= 3\n", + "ORDER BY visit_date;\n", + "\n", + "Error\n", + "0 / 15 testcases passed\n", + "You cannot nest a window function in the specification of window ''.\n", + "```\n", + "\n", + "> ただし上記は一部バージョンでオプティマイザが式の再計算を増やす可能性があるため、安定運用なら **CTE 2 段**(前掲の改善版)を推奨します。\n", + "\n", + "---\n", + "\n", + "## インデックス最適化\n", + "\n", + "フィルタが `people >= 100`、ウィンドウが `ORDER BY id`、出力で `visit_date` を返すため、次を推奨します。\n", + "\n", + "```sql\n", + "-- people で範囲抽出しつつ id の順序性を活かす\n", + "CREATE INDEX ix_stadium_people_id ON Stadium (people, id);\n", + "\n", + "-- さらにカバリングさせたいなら(ストレージと更新コストと相談)\n", + "CREATE INDEX ix_stadium_people_id_date ON Stadium (people, id, visit_date);\n", + "```\n", + "\n", + "効果:\n", + "\n", + "* `pre` で `people` 条件の範囲スキャン\n", + "* そのまま `id` 昇順の並びを得やすく、`ROW_NUMBER() OVER (ORDER BY id)` のソートコストを低減\n", + "* 最終 `ORDER BY visit_date` は別ソートになりますが、対象行は **島長 ≥ 3** に絞られているためコストは小さくなります\n", + " (要件的には `visit_date ASC` 必須ですが、仕様上「id ↑ ⇒ date ↑」なので、許容される環境なら `ORDER BY id` で等価にできます)\n", + "\n", + "---\n", + "\n", + "## 追加の微調整\n", + "\n", + "* データ量が少ない/中程度なら現状でも十分。大規模(数百万行〜)なら統計更新と `ANALYZE TABLE Stadium;` を適宜実施。\n", + "* CTE は MySQL 8 では多くの場合インライン化されますが、環境によっては派生表のマテリアライズが起きます。実行計画を見て重い場合は **派生表に `/*+ NO_MERGE() */` / `/*+ MERGE() */` ヒント**の検討(バージョン依存)も。\n", + "\n", + "---\n", + "\n", + "## まとめ\n", + "\n", + "* **JOIN を外し、ウィンドウ `COUNT()` で直接フィルタ**:短く速く\n", + "* **`(people, id[, visit_date])` の複合インデックス**:ソート・走査コスト削減\n", + "* これで一般に **数〜十数 % 程度の短縮**が見込めます(データ分布とバージョン次第)\n", + "\n", + "MySQL 8 は **ウィンドウ関数の“入れ子”を禁止**しており、`PARTITION BY (id - ROW_NUMBER() OVER (...))` のような書き方はできません。そのため、**`ROW_NUMBER()` を先に別レイヤーで計算してから**、外側で `COUNT() OVER (PARTITION BY ...)` を使う形に分解してください。\n", + "\n", + "## 動く修正版(派生表2段でネスト回避)\n", + "\n", + "```sql\n", + "SELECT id, visit_date, people\n", + "FROM (\n", + " SELECT\n", + " t.*,\n", + " COUNT(*) OVER (PARTITION BY (id - rn)) AS island_len\n", + " FROM (\n", + " SELECT\n", + " id,\n", + " visit_date,\n", + " people,\n", + " ROW_NUMBER() OVER (ORDER BY id) AS rn\n", + " FROM Stadium\n", + " WHERE people >= 100\n", + " ) AS t\n", + ") AS x\n", + "WHERE island_len >= 3\n", + "ORDER BY visit_date;\n", + "\n", + "Runtime 332 ms\n", + "Beats 80.65%\n", + "\n", + "```\n", + "\n", + "* 内側:`ROW_NUMBER()` を `rn` として計算\n", + "* 中間:`grp_key = id - rn` を式で作る(ここでは単なる通常列演算)\n", + "* 外側:`COUNT(*) OVER (PARTITION BY (id - rn))` で島の長さを算出して `>= 3` を抽出\n", + "\n", + "> ポイント:**ウィンドウ関数の引数や `PARTITION BY` 式の中に別のウィンドウ関数を置かない**こと。必ず一段外に出してから使う。\n", + "\n", + "## CTE 版(読みやすさ重視・推奨)\n", + "\n", + "```sql\n", + "WITH pre AS (\n", + " SELECT id, visit_date, people\n", + " FROM Stadium\n", + " WHERE people >= 100\n", + "),\n", + "grp AS (\n", + " SELECT\n", + " id,\n", + " visit_date,\n", + " people,\n", + " id - ROW_NUMBER() OVER (ORDER BY id) AS grp_key\n", + " FROM pre\n", + ")\n", + "SELECT id, visit_date, people\n", + "FROM (\n", + " SELECT\n", + " g.*,\n", + " COUNT(*) OVER (PARTITION BY grp_key) AS island_len\n", + " FROM grp AS g\n", + ") x\n", + "WHERE island_len >= 3\n", + "ORDER BY visit_date;\n", + "```\n", + "\n", + "こちらは既にご提案済みの「JOIN 省略版」で、**ネストなし**・可読性良好です。\n", + "\n", + "---\n", + "\n", + "### 参考メモ\n", + "\n", + "* MySQL 8 の制約:`You cannot nest a window function in the specification of window ...`\n", + " → **サブクエリ(派生表 or CTE)で段階計算**が定石です。\n", + "* パフォーマンス面では、どちらの書き方も**結合を無くし、スキャン回数を減らせる**ため、先の `big_islands` 版より有利になりやすいです。\n", + "* 追加最適化:`CREATE INDEX ix_stadium_people_id ON Stadium(people, id);` は引き続き有効です。\n", + "\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/SQL/Leetcode/Advanced select/601. Human Traffic of Stadium/gpt/Human_Traffic_of_Stadium_pandas.ipynb b/SQL/Leetcode/Advanced select/601. Human Traffic of Stadium/gpt/Human_Traffic_of_Stadium_pandas.ipynb new file mode 100644 index 00000000..5f164c70 --- /dev/null +++ b/SQL/Leetcode/Advanced select/601. Human Traffic of Stadium/gpt/Human_Traffic_of_Stadium_pandas.ipynb @@ -0,0 +1,223 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Pandas 2.2.2用\n", + "\n", + "## 0) 前提\n", + "\n", + "* 環境: **Python 3.10.15 / pandas 2.2.2**\n", + "* **指定シグネチャ厳守**(この回答では `stadium_consecutive` を定義)\n", + "* I/O 禁止、不要な `print` や `sort_values` 禁止(必要な並び替えは **NumPy の `argsort`** で実施)\n", + "* 判定は **ID 連続**、出力は **`id, visit_date, people`**(`visit_date` 昇順)\n", + "\n", + "## 1) 問題\n", + "\n", + "* `3 行以上の「連続した id」を持ち、かつ各行 people >= 100 のレコードを抽出する。結果は visit_date 昇順。`\n", + "* 入力 DF: `stadium(id: int, visit_date: datetime64[ns] or date-like, people: int)`\n", + "* 出力: `['id','visit_date','people']` — 連続 ID の島(長さ ≥ 3)に属する行のみ\n", + "\n", + "## 2) 実装(指定シグネチャ厳守)\n", + "\n", + "> 列最小化 → `people >= 100` で縮小 → `id - row_number(order by id)` で島キー → グループサイズでフィルタ → `visit_date` 昇順に整形(`np.argsort`)。\n", + "\n", + "```python\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "def stadium_consecutive(stadium: pd.DataFrame) -> pd.DataFrame:\n", + " \"\"\"\n", + " Returns:\n", + " pd.DataFrame: 列名と順序は ['id', 'visit_date', 'people']\n", + " \"\"\"\n", + " # 必要列のみ\n", + " df = stadium[['id', 'visit_date', 'people']]\n", + "\n", + " # people >= 100 のみ対象(以降の計算対象を縮小)\n", + " df_hot = df[df['people'] >= 100]\n", + "\n", + " if df_hot.empty:\n", + " # 空ならそのまま空の所定列を返す\n", + " return df_hot[['id', 'visit_date', 'people']]\n", + "\n", + " # ---- row_number() over (order by id) を numpy で実装(sort_values を使わない) ----\n", + " ids = df_hot['id'].to_numpy()\n", + " order_idx = np.argsort(ids, kind='mergesort') # 安定ソート\n", + " rn = np.empty_like(order_idx) # rn を id の順番に 1..n で割り振る\n", + " rn[order_idx] = np.arange(1, order_idx.size + 1)\n", + "\n", + " # 連番島キー: id - row_number\n", + " grp_key = ids - rn\n", + "\n", + " # 島の長さを算出(groupby-aggregate → transform(size) 相当)\n", + " # 高速化のため pandas の groupby を使う\n", + " island_len = (\n", + " pd.Series(grp_key, index=df_hot.index)\n", + " .groupby(grp_key)\n", + " .transform('size')\n", + " .to_numpy()\n", + " )\n", + "\n", + " # 長さ >= 3 の島に属する行のみ\n", + " mask = island_len >= 3\n", + " out = df_hot.loc[mask, ['id', 'visit_date', 'people']]\n", + "\n", + " if out.empty:\n", + " return out\n", + "\n", + " # 最終並び: visit_date 昇順(sort_values を使わず numpy で整列)\n", + " order_v = np.argsort(out['visit_date'].to_numpy(), kind='mergesort')\n", + " out = out.iloc[order_v]\n", + "\n", + " # インデックスは連番に整える(表示の安定性向上)\n", + " out = out.reset_index(drop=True)\n", + " return out\n", + "\n", + "Analyze Complexity\n", + "Runtime 310 ms\n", + "Beats 59.45%\n", + "Memory 67.59 MB\n", + "Beats 60.59%\n", + "\n", + "```\n", + "\n", + "## 3) アルゴリズム説明\n", + "\n", + "* 使用 API / 手法\n", + "\n", + " * `np.argsort` で **row_number の代替**(`id` 昇順の序数を付与)\n", + " * `id - row_number` で **Gaps-and-Islands** の島キーを作成\n", + " * `groupby(...).transform('size')` で島の長さ(各行に島サイズを付与)\n", + " * しきい値(≥3)で抽出\n", + " * 仕上げの並び替えは `np.argsort`(`sort_values` 非使用)\n", + "* **NULL / 重複 / 型**\n", + "\n", + " * `visit_date` は date/datetime いずれでも `np.argsort` で昇順化可能(NaT があれば末尾寄りになりますが、本問想定ではユニークで欠損なし)\n", + " * `id` は連番で単調増加が前提(テーブル仕様より)。同値行がないので `row_number` は一意\n", + " * `people` の判定は整数比較(欠損がある場合は事前に `fillna(-inf)` 等で対処)\n", + "\n", + "## 4) 計算量(概算)\n", + "\n", + "* `np.argsort`(`id` と `visit_date` の 2 回): **O(N log N)**\n", + "* `groupby(...).transform('size')`: **O(N)**\n", + "* 総計: **O(N log N)**、メモリは `O(N)` で補助配列(整数/小規模)を使用\n", + "\n", + "## 5) 図解(Mermaid 超保守版)\n", + "\n", + "```mermaid\n", + "flowchart TD\n", + " A[入力 DataFrame stadium] --> B[people >= 100 で縮小]\n", + " B --> C[id の row_number を算出]\n", + " C --> D[id - row_number で島キー]\n", + " D --> E[島ごとのサイズ >= 3 を抽出]\n", + " E --> F[visit_date 昇順で整形]\n", + " F --> G[出力 id, visit_date, people]\n", + "```\n", + "\n", + "**さらに速く・省メモリ**にできます。ボトルネックは `groupby(...).transform('size')` と Series/CTE 的な中間オブジェクトです。\n", + "PostgreSQL 版と同じ発想(連番の島=**gaps-and-islands**)を、**純 NumPy** で実装するとだいぶ軽くなります。\n", + "\n", + "下は **`sort_values` 非使用**・**I/O/print なし**・**シグネチャ厳守** の最適化版です。\n", + "\n", + "```python\n", + "import numpy as np\n", + "import pandas as pd\n", + "\n", + "def stadium_consecutive(stadium: pd.DataFrame) -> pd.DataFrame:\n", + " \"\"\"\n", + " Returns:\n", + " pd.DataFrame: 列名と順序は ['id', 'visit_date', 'people']\n", + " \"\"\"\n", + " # 必要列だけ抽出(ビュー寄りに揃える)\n", + " df = stadium[['id', 'visit_date', 'people']]\n", + "\n", + " # people >= 100 の行だけに縮小(ブール配列でコピーを極力回避)\n", + " hot_mask = df['people'].to_numpy() >= 100\n", + " if not np.any(hot_mask):\n", + " return df.iloc[0:0][['id', 'visit_date', 'people']]\n", + "\n", + " hot = df.loc[hot_mask, ['id', 'visit_date', 'people']]\n", + "\n", + " # ---- ここからは NumPy ベースで連続 ID の島を検出 ----\n", + " ids = hot['id'].to_numpy()\n", + " # row_number 相当は不要。id をソートして連続 run を直接求める\n", + " order = np.argsort(ids, kind='quicksort') # 安定性不要なので quicksort\n", + " ids_sorted = ids[order]\n", + "\n", + " # 連続ブレークを検知: 先頭は必ずブレーク\n", + " # diff==1 が「連続」、それ以外がブレーク\n", + " n = ids_sorted.size\n", + " brk = np.empty(n, dtype=bool)\n", + " brk[0] = True\n", + " if n > 1:\n", + " brk[1:] = np.diff(ids_sorted) != 1\n", + "\n", + " # ラベル付け(累積和で run ID を振る): 0..G-1\n", + " run_id = np.cumsum(brk) - 1\n", + "\n", + " # 各 run の長さ(bincount は速くて省メモリ)\n", + " run_len = np.bincount(run_id)\n", + " keep_sorted = run_len[run_id] >= 3\n", + "\n", + " if not np.any(keep_sorted):\n", + " return hot.iloc[0:0][['id', 'visit_date', 'people']]\n", + "\n", + " # ソート順から元の行インデックスへ戻す\n", + " kept_idx = order[keep_sorted]\n", + " out = hot.iloc[kept_idx, :] # 列順は ['id','visit_date','people'] のまま\n", + "\n", + " # 指定:visit_date 昇順(sort_values は使わず np.argsort)\n", + " vorder = np.argsort(out['visit_date'].to_numpy(), kind='quicksort')\n", + " out = out.iloc[vorder].reset_index(drop=True)\n", + "\n", + " return out[['id', 'visit_date', 'people']]\n", + "\n", + "Analyze Complexity\n", + "Runtime 306 ms\n", + "Beats 65.60%\n", + "Memory 67.32 MB\n", + "Beats 81.78%\n", + "\n", + "```\n", + "\n", + "---\n", + "\n", + "## 改善ポイント(なぜ速い?)\n", + "\n", + "* **`groupby.transform('size')` を排除**\n", + " → `np.diff` + `np.cumsum` + `np.bincount` の **O(N)** 構成に置換。中間 Series も削減。\n", + "* **`ROW_NUMBER` 風の計算をやめ、run-length encoding で直接「連続」を検出**\n", + " → 追加の配列1〜2本で済み、**メモリ節約**(特に N が大きいと効く)。\n", + "* **`mergesort`→`quicksort`**\n", + " → 安定性不要な箇所はクイックソートで **純粋に速い**。\n", + "* **列最小化**(最初から `['id','visit_date','people']` のみ)\n", + " → キャッシュ効率と一時オブジェクト削減。\n", + "\n", + "---\n", + "\n", + "## さらに詰めるなら(任意)\n", + "\n", + "* `id` / `people` を `int32` にダウンサイジング可能なら、事前に `astype('int32')` で**約半分のメモリ**。\n", + " (ただし上限値と欠損の有無に注意)\n", + "* `visit_date` が文字列なら **事前に `datetime64[ns]` 化**で `argsort` が高速化。\n", + "* 入力 DF が巨大で別処理も走る環境なら、**列アクセス時に `to_numpy(copy=False)`** を積極利用しコピー回避。\n", + "\n", + "---\n", + "\n", + "### 期待効果(目安)\n", + "\n", + "* 同等のデータ分布で、`groupby.transform('size')` 版から **10〜30% 程度の短縮**、\n", + " メモリは **Series/GroupBy の一時領域分が削減**されるケースが多いです。\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/SQL/Leetcode/Advanced select/601. Human Traffic of Stadium/gpt/Human_Traffic_of_Stadium_posgres.ipynb b/SQL/Leetcode/Advanced select/601. Human Traffic of Stadium/gpt/Human_Traffic_of_Stadium_posgres.ipynb new file mode 100644 index 00000000..41f185c7 --- /dev/null +++ b/SQL/Leetcode/Advanced select/601. Human Traffic of Stadium/gpt/Human_Traffic_of_Stadium_posgres.ipynb @@ -0,0 +1,271 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# PostgreSQL 16.6+\n", + "\n", + "## 0) 前提\n", + "\n", + "* エンジン: **PostgreSQL 16.6+**\n", + "* 並び順: 任意\n", + " ※本問題は仕様で **`visit_date` 昇順** が要求されるため最終的に付与\n", + "* `NOT IN` 回避(`EXISTS` / `LEFT JOIN ... IS NULL` を推奨)※本問では未使用\n", + "* 判定は **ID 連続**、表示は仕様どおりの列順\n", + "\n", + "## 1) 問題\n", + "\n", + "* `3 行以上の「連続した id」を持ち、かつ各行 people >= 100 のレコードを抽出する。結果は visit_date 昇順。`\n", + "* 入力: `Stadium(id int, visit_date date, people int)`(`visit_date` はユニーク、id 上昇とともに日付も上昇)\n", + "* 出力: `id, visit_date, people`(条件を満たす「連続 id の島」に属するすべての行)\n", + "\n", + "## 2) 最適解(単一クエリ)\n", + "\n", + "> まず `people >= 100` で縮小し、`id - ROW_NUMBER()` で **連番の島キー** を作成。\n", + "> その島の長さを `COUNT(*) OVER (PARTITION BY grp_key)` で求め、長さ ≥ 3 のみ残す。\n", + "\n", + "```sql\n", + "WITH pre AS (\n", + " SELECT id, visit_date, people\n", + " FROM stadium\n", + " WHERE people >= 100\n", + "),\n", + "grp AS (\n", + " SELECT\n", + " id,\n", + " visit_date,\n", + " people,\n", + " id - ROW_NUMBER() OVER (ORDER BY id) AS grp_key\n", + " FROM pre\n", + ")\n", + "SELECT id, visit_date, people\n", + "FROM (\n", + " SELECT\n", + " g.*,\n", + " COUNT(*) OVER (PARTITION BY grp_key) AS island_len\n", + " FROM grp AS g\n", + ") x\n", + "WHERE island_len >= 3\n", + "ORDER BY visit_date;\n", + "\n", + "Runtime 188 ms\n", + "Beats 58.33%\n", + "\n", + "```\n", + "\n", + "### 代替(`LAG` でブレーク検知 → 累積和で島採番)\n", + "\n", + "```sql\n", + "WITH pre AS (\n", + " SELECT id, visit_date, people\n", + " FROM stadium\n", + " WHERE people >= 100\n", + "),\n", + "marked AS (\n", + " SELECT\n", + " id,\n", + " visit_date,\n", + " people,\n", + " CASE WHEN id = LAG(id) OVER (ORDER BY id) + 1 THEN 0 ELSE 1 END AS is_break\n", + " FROM pre\n", + "),\n", + "islands AS (\n", + " SELECT\n", + " id,\n", + " visit_date,\n", + " people,\n", + " SUM(is_break) OVER (ORDER BY id) AS grp_key\n", + " FROM marked\n", + ")\n", + "SELECT id, visit_date, people\n", + "FROM (\n", + " SELECT\n", + " i.*,\n", + " COUNT(*) OVER (PARTITION BY grp_key) AS island_len\n", + " FROM islands i\n", + ") s\n", + "WHERE island_len >= 3\n", + "ORDER BY visit_date;\n", + "\n", + "Runtime 193 ms\n", + "Beats 47.52%\n", + "```\n", + "\n", + "> Postgres はウィンドウ関数の **入れ子(window の中で別の window)** を許しませんが、上記はいずれも **段階的に計算**しているため問題ありません。\n", + "\n", + "## 3) 要点解説\n", + "\n", + "* **Gaps-and-Islands** の定石\n", + "\n", + " * `id - ROW_NUMBER()` が一定 ⇒ **連続 id の島**\n", + " * その島のサイズを `COUNT() OVER (PARTITION BY grp_key)` で評価\n", + "* 先に `WHERE people >= 100` で絞ることで、ソート対象行を減らしウィンドウ計算を軽量化\n", + "* 並びは仕様に合わせ **`ORDER BY visit_date`**(`id` と日付の昇順は一致するが、仕様優先)\n", + "\n", + "> インデックス(任意だが大規模なら推奨)\n", + "> `CREATE INDEX ON stadium (people, id);`\n", + "> 先頭 `people` で絞り、続く `id` 順でソート負荷を軽減\n", + "\n", + "## 4) 計算量(概算)\n", + "\n", + "* 対象行数を `n` とすると\n", + "\n", + " * ウィンドウ(`ROW_NUMBER` / `LAG`): **O(n log n)**(`ORDER BY id` のソート)\n", + " * `COUNT() OVER (PARTITION BY grp_key)`: **O(n)**(パーティション内の線形集計)\n", + "* 適切なインデックスにより実効コストはさらに低減\n", + "\n", + "## 5) 図解(Mermaid 超保守版)\n", + "\n", + "```mermaid\n", + "flowchart TD\n", + " A[入力 Stadium] --> B[前処理 people >= 100]\n", + " B --> C[ROW_NUMBER または LAG で島キー作成]\n", + " C --> D[COUNT OVER で島サイズ算出]\n", + " D --> E[島サイズ >= 3 を抽出]\n", + " E --> F[visit_date 昇順で出力]\n", + "```\n", + "\n", + "1. **JOIN/再集計を増やさずに “島の長さでフィルタ” している点は最適**\n", + "  → そのままでOK。CTEはPG16では基本インライン化されます(`MATERIALIZED`を付けない限り)。\n", + "\n", + "2. **並び順の最適化**\n", + "\n", + "* 仕様が許せば `ORDER BY id` に置換(`id ↑ ⇒ visit_date ↑` が保証されているため等価)。\n", + " これで `visit_date` での**追加ソートを回避**でき、`people, id`系のインデックスから**ソート不要**で出せます。\n", + "* 仕様上どうしても `visit_date` 必須なら現行のまま。ただし対象行は「島長≥3」に絞られているのでコスト影響は限定的。\n", + "\n", + "3. **インデックス設計(最重要)**\n", + " 実測時間の差は**インデックス有無・形**で大きく変わります。\n", + "\n", + "```sql\n", + "-- people で範囲 → id の順序性を活かす\n", + "CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_stadium_people_id ON stadium (people, id);\n", + "\n", + "-- “該当行だけ” を速く触りたいなら部分インデックス(最有力)\n", + "CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_stadium_hot ON stadium (id) WHERE people >= 100;\n", + "\n", + "-- visit_date で最終ソートが厳しい場合のカバリング(任意・サイズと相談)\n", + "CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_stadium_people_id_date ON stadium (people, id, visit_date);\n", + "```\n", + "\n", + "* **部分インデックス `(id) WHERE people >= 100`** は今回のフィルタにドンピシャ。\n", + " `pre` の抽出・`ORDER BY id` のウィンドウ両方が軽くなります。\n", + "* すでに巨大データなら `ANALYZE stadium;` で統計を最新化。\n", + "\n", + "---\n", + "\n", + "## 速度最優先の等価クエリ(`ORDER BY id` 可の場合)\n", + "\n", + "```sql\n", + "WITH pre AS (\n", + " SELECT id, visit_date, people\n", + " FROM stadium\n", + " WHERE people >= 100\n", + "),\n", + "grp AS (\n", + " SELECT\n", + " id,\n", + " visit_date,\n", + " people,\n", + " id - ROW_NUMBER() OVER (ORDER BY id) AS grp_key\n", + " FROM pre\n", + ")\n", + "SELECT id, visit_date, people\n", + "FROM (\n", + " SELECT\n", + " g.*,\n", + " COUNT(*) OVER (PARTITION BY grp_key) AS island_len\n", + " FROM grp AS g\n", + ") x\n", + "WHERE island_len >= 3\n", + "ORDER BY id; -- 許されるならこちらが速い\n", + "\n", + "Runtime 184 ms\n", + "Beats 69.15%\n", + "\n", + "```\n", + "\n", + "* `ix_stadium_hot`(部分インデックス)があると、**範囲抽出+連番ウィンドウ**が非常に効きます。\n", + "* ほぼ同じ書きぶりで、最終ソート回避による**数〜数十%の短縮**が見込めます(データ分布次第)。\n", + "\n", + "---\n", + "\n", + "## ウィンドウ不使用の高速代替(自己相関 `EXISTS` 三点チェック)\n", + "\n", + "> 「島に属する行」を、“自分の前後に連続が2つあるか” の**3パターン**で判定。\n", + "> ウィンドウが重い分布ではこちらが速いことがあります(インデックス前提)。\n", + "\n", + "```sql\n", + "SELECT s.id, s.visit_date, s.people\n", + "FROM stadium s\n", + "WHERE s.people >= 100\n", + "AND (\n", + " -- s, s-1, s-2 が連続\n", + " EXISTS (\n", + " SELECT 1 FROM stadium a\n", + " WHERE a.id = s.id - 1 AND a.people >= 100\n", + " AND EXISTS (\n", + " SELECT 1 FROM stadium b\n", + " WHERE b.id = s.id - 2 AND b.people >= 100\n", + " )\n", + " )\n", + " -- s, s+1, s+2 が連続\n", + " OR EXISTS (\n", + " SELECT 1 FROM stadium a\n", + " WHERE a.id = s.id + 1 AND a.people >= 100\n", + " AND EXISTS (\n", + " SELECT 1 FROM stadium b\n", + " WHERE b.id = s.id + 2 AND b.people >= 100\n", + " )\n", + " )\n", + " -- s-1, s, s+1 が連続(中央)\n", + " OR (\n", + " EXISTS (SELECT 1 FROM stadium a WHERE a.id = s.id - 1 AND a.people >= 100)\n", + " AND EXISTS (SELECT 1 FROM stadium b WHERE b.id = s.id + 1 AND b.people >= 100)\n", + " )\n", + ")\n", + "ORDER BY /* visit_date */ id; -- 仕様に合わせて変更\n", + "\n", + "Runtime 476 ms\n", + "Beats 5.26%\n", + "\n", + "```\n", + "\n", + "**ポイント**\n", + "\n", + "* 連続IDの島(長さ≥3)に属する行は、上記いずれかの三条件を満たします。\n", + "* インデックスは **`(id)` の部分インデックス `WHERE people >= 100`** が効きます。\n", + "* ウィンドウなし・ソート最小化により、**高選択度**かつ**疎な分布**では特に有利。\n", + "\n", + "---\n", + "\n", + "## 追加の微調整\n", + "\n", + "* `pre`/`grp` はPG16では**自動インライン**が既定。重い場合のみ\n", + "\n", + " * 再利用が多い中間を強制マテリアライズ → `WITH ... AS MATERIALIZED`\n", + " * 逆にインラインしたい → `WITH ... AS NOT MATERIALIZED`(既定なので通常不要)\n", + "* 実行直前にだけ `SET work_mem = '64MB';` などで**ソート用メモリ**を増やすと、ディスク落ち回避で安定短縮。\n", + "\n", + "---\n", + "\n", + "### まとめ\n", + "\n", + "* まずは **部分インデックス `(id) WHERE people >= 100`** を作成\n", + "* 仕様が許すなら **`ORDER BY id`** に変更(無理なら現行維持)\n", + "* データ分布次第で **EXISTS 版** も計測し、速い方を採用\n", + "\n", + "この3点で、提示の **188ms → 1〜3割程度の短縮**が現実的に狙えます(環境・分布依存)。\n", + "\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/prettier.config.js b/prettier.config.js deleted file mode 100644 index 44d77448..00000000 --- a/prettier.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/** @type {import("prettier").Config} */ -export default { - semi: true, // 文末にセミコロンを付ける - singleQuote: true, // シングルクォートを使用 - trailingComma: 'all', // 複数行の配列・オブジェクトなどの末尾にカンマを付ける - tabWidth: 4, // インデント幅は2スペース - useTabs: false, // スペースでインデント - printWidth: 100, // 1行の最大文字数(超えると改行される) - bracketSpacing: true, // オブジェクトリテラルの中にスペースを入れる: { foo: bar } - arrowParens: 'always', // アロー関数の引数に括弧を常に付ける - endOfLine: 'lf', // 改行コードをLFに統一(Gitなどで混乱を避けるため) -}; -