diff --git a/SQL/Leetcode/Basic select/1148. Article Views I/chatGPT 5.1 thinking customized/Article_Views_I_pandas.ipynb b/SQL/Leetcode/Basic select/1148. Article Views I/chatGPT 5.1 thinking customized/Article_Views_I_pandas.ipynb new file mode 100644 index 00000000..a01cafc9 --- /dev/null +++ b/SQL/Leetcode/Basic select/1148. Article Views I/chatGPT 5.1 thinking customized/Article_Views_I_pandas.ipynb @@ -0,0 +1,462 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "016102a0", + "metadata": {}, + "source": [ + "## 0) 前提\n", + "\n", + "* 環境: **Python 3.10.15 / pandas 2.2.2**\n", + "* **指定シグネチャ厳守**: `def article_views(views: pd.DataFrame) -> pd.DataFrame:`\n", + "* I/O 禁止(`read_csv`, `to_csv` などなし)\n", + "* 不要な `print` や **`sort_values` 禁止**(別手段でソート)\n", + "\n", + "---\n", + "\n", + "## 1) 問題\n", + "\n", + "* `{{PROBLEM_STATEMENT}}`\n", + " 著者が自分の書いた記事を **少なくとも 1 回** 閲覧した著者の ID を求める。\n", + " `author_id = viewer_id` となる行が「自分の記事を閲覧した」ことを意味する。\n", + "\n", + "* 入力 DF: `{{INPUT_DATAFRAMES}}`\n", + "\n", + " ```text\n", + " views: pd.DataFrame\n", + "\n", + " columns:\n", + " - article_id : int\n", + " - author_id : int\n", + " - viewer_id : int\n", + " - view_date : datetime64[ns] / object(日付相当)\n", + " ```\n", + "\n", + "* 出力: `{{OUTPUT_COLUMNS_AND_RULES}}`\n", + "\n", + " ```text\n", + " columns:\n", + " - id : int # 自分の記事を閲覧した著者 ID\n", + "\n", + " 仕様:\n", + " - 各著者は 1 回のみ(重複なし)\n", + " - id 昇順ソート\n", + " ```\n", + "\n", + "---\n", + "\n", + "## 2) 実装(指定シグネチャ厳守)\n", + "\n", + "> この問題は **グループ集計不要** で、\n", + "> 「条件フィルタ → 著者IDのユニーク化 → ソート」が最軽量パターンです。\n", + "\n", + "```python\n", + "import pandas as pd\n", + "\n", + "def article_views(views: pd.DataFrame) -> pd.DataFrame:\n", + " \"\"\"\n", + " Returns:\n", + " pd.DataFrame: 列名と順序は ['id']\n", + " \"\"\"\n", + " # 著者 = 閲覧者 となる行だけを抽出(自分の記事を閲覧した行)\n", + " self_views = views[views[\"author_id\"] == views[\"viewer_id\"]]\n", + "\n", + " # 著者IDのみを取り出し、ユニーク化して列名を id に変更\n", + " out = (\n", + " self_views[[\"author_id\"]]\n", + " .drop_duplicates()\n", + " .rename(columns={\"author_id\": \"id\"})\n", + " )\n", + "\n", + " # sort_values 禁止のため、インデックスにして sort_index で昇順ソート\n", + " out = out.set_index(\"id\").sort_index().reset_index()\n", + "\n", + " return out\n", + "\n", + "Analyze Complexity\n", + "Runtime 281 ms\n", + "Beats 60.21%\n", + "Memory 67.53 MB\n", + "Beats 29.19%\n", + "\n", + "```\n", + "\n", + "* ポイント:\n", + "\n", + " * **列最小化**:`[\"author_id\"]` のみ扱うことでメモリも処理も軽くする\n", + " * **`drop_duplicates()`** で著者 ID を一意に\n", + " * 明示的な `sort_values` を避け、`set_index` + `sort_index` で昇順を実現\n", + "\n", + "---\n", + "\n", + "## 3) アルゴリズム説明\n", + "\n", + "使用 API:\n", + "\n", + "1. **ブールインデックス**\n", + "\n", + " ```python\n", + " views[\"author_id\"] == views[\"viewer_id\"]\n", + " ```\n", + "\n", + " * 著者と閲覧者が同一の行のみ True\n", + " * `views[condition]` で「自分の記事を閲覧した」ログだけを取り出す\n", + "\n", + "2. **列最小化**\n", + "\n", + " ```python\n", + " self_views[[\"author_id\"]]\n", + " ```\n", + "\n", + " * 不要な列を落とし、後続処理(重複削除・ソート)を軽量化\n", + "\n", + "3. **重複削除 `drop_duplicates`**\n", + "\n", + " ```python\n", + " .drop_duplicates()\n", + " ```\n", + "\n", + " * 同じ著者が何度も自分の記事を見ていても 1 行にまとめる\n", + " * 著者 ID のユニーク集合が得られる\n", + "\n", + "4. **`rename` による列名変更**\n", + "\n", + " ```python\n", + " .rename(columns={\"author_id\": \"id\"})\n", + " ```\n", + "\n", + " * 問題仕様の列名 `id` に合わせる\n", + "\n", + "5. **`set_index` + `sort_index` による昇順ソート**\n", + "\n", + " ```python\n", + " out = out.set_index(\"id\").sort_index().reset_index()\n", + " ```\n", + "\n", + " * `id` を一度インデックスにしてから `sort_index()` でソート\n", + " * `sort_values` を使わずに昇順を実現\n", + " * 最後に `reset_index()` で通常列として `id` を戻す\n", + "\n", + "### NULL / 重複 / 型 について\n", + "\n", + "* `author_id` / `viewer_id` が NULL の場合\n", + " → `==` 比較は `False` になり、その行は自然に除外される\n", + " (「著者不明」や「閲覧者不明」は「自分の記事を見た」と判断できないので妥当)\n", + "* `drop_duplicates()` は完全一致行を 1 行にまとめるだけなので、\n", + " 「同じ著者 ID」が複数行あっても安全にユニーク化できる。\n", + "* 型は `int` 前提だが、文字列でもロジック自体は同じく動作する。\n", + "\n", + "---\n", + "\n", + "## 4) 計算量(概算)\n", + "\n", + "`N` = `views` の行数とすると:\n", + "\n", + "1. **ブールフィルタ**\n", + "\n", + " ```python\n", + " views[\"author_id\"] == views[\"viewer_id\"]\n", + " ```\n", + "\n", + " * 全行を 1 回見るので **O(N)**\n", + "\n", + "2. **列抽出 + `drop_duplicates`**\n", + "\n", + " * 列抽出自体はビュー作成に近く、ほぼ **O(N)**(参照コピー主体)\n", + " * `drop_duplicates` はハッシュセットベースでおおよそ **O(N)**\n", + "\n", + "3. **`set_index` + `sort_index`**\n", + "\n", + " * インデックス設定: **O(N)**\n", + " * ソート: **O(M log M)** (M は自分の記事を閲覧した著者数、通常 N より小さい)\n", + "\n", + "トータルで概ね **O(N + M log M)**。\n", + "M ≪ N であればほぼ線形に近いコストで動作します。\n", + "\n", + "---\n", + "\n", + "## 5) 図解(Mermaid 超保守版)\n", + "\n", + "```mermaid\n", + "flowchart TD\n", + " A[\"views DataFrame\n", + " (article_id, author_id, \n", + " viewer_id, view_date)\"] --> B[author_id = viewer_id\n", + " でフィルタ]\n", + " B --> C[author_id 列だけ抽出\n", + " 列最小化]\n", + " C --> D[drop_duplicates で\n", + " 著者IDのユニーク集合]\n", + " D --> E[列名を id に rename]\n", + " E --> F[\"set_index(id) → sort_index()\n", + " → reset_index()\"]\n", + " F --> G[出力: id 列のみの\n", + " DataFrame]\n", + "```\n", + "\n", + "この実装で、LeetCode の Pandas 版要件(シグネチャ、列名、順序、I/O 無し、`sort_values` 不使用)をすべて満たしつつ、処理も十分に軽量に保てます。\n", + "\n", + "結論から言うと、そのスコアは**かなり良い部類**で、アルゴリズムとして大きな改善余地はほぼありませんが、\n", + "「pandas のオーバーヘッドを少しでも削る」という意味での**微調整**はできます 💡\n", + "\n", + "---\n", + "\n", + "## 1. いまの実装の位置づけ\n", + "\n", + "前回お渡しした実装はだいたいこんな流れでしたね:\n", + "\n", + "```python\n", + "self_views = views[views[\"author_id\"] == views[\"viewer_id\"]]\n", + "\n", + "out = (\n", + " self_views[[\"author_id\"]]\n", + " .drop_duplicates()\n", + " .rename(columns={\"author_id\": \"id\"})\n", + ")\n", + "\n", + "out = out.set_index(\"id\").sort_index().reset_index()\n", + "```\n", + "\n", + "アルゴリズムとしてはすでに最適レベルで、\n", + "\n", + "* 1 回のフィルタ(O(N))\n", + "* 著者 ID ユニーク化(O(N))\n", + "* 著者数 M に対してソート(O(M log M))\n", + "\n", + "という感じで、これ以上 “理論オーダー” を改善する余地はありません。\n", + "\n", + "LeetCode の `Runtime` / `Beats` は\n", + "**同時期に走っている他ユーザーやマシン負荷の影響でかなりブレる**ので、\n", + "281ms / 60% は普通に「十分速い」です。\n", + "\n", + "ただ、**pandas の高機能メソッドを少し減らす**ことで、\n", + "オーバーヘッドを減らす = 数 ms〜十数 ms 程度縮む可能性はあります。\n", + "\n", + "---\n", + "\n", + "## 2. pandas を薄くしてみる改善案(おすすめ)\n", + "\n", + "ポイントは:\n", + "\n", + "* **DataFrame の生成・コピーを減らす**\n", + "* **Series / ndarray ベースで処理**\n", + "* `drop_duplicates` や `set_index` を避ける\n", + "* `sort_values` 禁止ルールは守る\n", + "\n", + "こうすると、こんな感じにできます。\n", + "\n", + "```python\n", + "import pandas as pd\n", + "\n", + "def article_views(views: pd.DataFrame) -> pd.DataFrame:\n", + " \"\"\"\n", + " Returns:\n", + " pd.DataFrame: 列名と順序は ['id']\n", + " \"\"\"\n", + " # ❶ 著者 = 閲覧者 の行だけブール配列で抽出\n", + " mask = views[\"author_id\"].values == views[\"viewer_id\"].values\n", + "\n", + " # ❷ 対象行の author_id を 1 次元配列として取得\n", + " # unique() は ndarray を返すので、そのまま扱える\n", + " ids = views.loc[mask, \"author_id\"].unique()\n", + "\n", + " # ❸ id 昇順ソート(ndarray に対してそのまま in-place sort)\n", + " ids.sort()\n", + "\n", + " # ❹ DataFrame 化(1 回だけ)\n", + " return pd.DataFrame({\"id\": ids})\n", + "```\n", + "\n", + "### なにが軽くなっているか\n", + "\n", + "* `self_views = views[...]` という **中間 DataFrame を作っていない**\n", + "* `drop_duplicates` ではなく、`unique()` で **Series → ndarray 1 本** にしている\n", + "* `set_index` / `sort_index` / `reset_index` といった\n", + " **インデックス操作を全部削っている**\n", + "* `sort_values` も使っていない(`ndarray.sort()`)\n", + "\n", + "処理フローとしては:\n", + "\n", + "1. `mask`(`bool` 配列)だけ作る → DataFrame に比べて軽い\n", + "2. `views.loc[mask, \"author_id\"].unique()` で **1 本の ndarray** を作る\n", + "3. その ndarray を **in-place sort**\n", + "4. 最後に 1 回だけ DataFrame を作る\n", + "\n", + "ので、**中間オブジェクトの量とコピー回数がかなり減っています**。\n", + "\n", + "実行時間・メモリともに、少しだけ改善する可能性が高いです。\n", + "\n", + "---\n", + "\n", + "## 3. メモリ 67MB / 29% について\n", + "\n", + "LeetCode の「Memory Beats X%」は、\n", + "\n", + "* Python のランタイム / JIT 的な Overhead\n", + "* テストケースのサイズ\n", + "* 他ユーザーのコードのメモリ特性\n", + "\n", + "などが絡むので、**純粋にあなたの関数だけのメモリ使用量を正確に示しているわけではありません**。\n", + "\n", + "今回の関数は\n", + "\n", + "* 入力 DF `views` をそのまま利用\n", + "* 中間としては `mask`(bool 配列)と `ids`(1 次元の著者配列)\n", + "* 最後に `{\"id\": ids}` の 1 列データフレーム\n", + "\n", + "だけなので、**ロジック的にはかなりメモリ効率も良い部類**です。\n", + "\n", + "これ以上メモリを削るとしたら\n", + "\n", + "* `mask` を作らずに `query` を使う(→ 逆に遅く・重くなることが多い)\n", + "* 純粋な `numpy` だけでやる(→ LeetCode 的にはやりすぎ)\n", + "\n", + "など、現実にはあまり得をしない方向になるので、\n", + "**今の改善版くらいがちょうどバランスの良い落としどころ**だと思ってもらって大丈夫です。\n", + "\n", + "---\n", + "\n", + "## 4. まとめ\n", + "\n", + "* 281ms / 60% / 67MB / 29% は **全然悪くないどころか十分合格点** ✅\n", + "* アルゴリズム(O(N + M log M))はすでに最適クラスで、\n", + " これ以上は **pandas のオーバーヘッド削減による微調整**の世界\n", + "* 上の `mask + unique + ndarray.sort` 版は\n", + "\n", + " * 中間 DataFrame を作らない\n", + " * `drop_duplicates` / `set_index` / `sort_index` / `reset_index` を使わない\n", + " という点で、**若干速く・若干メモリも軽くなる余地があります**。\n", + "\n", + "もしこの改善版を試して、また Runtime / Memory の結果が出たら、それをベースにさらに細かいチューニング(例えば `values` の使い方やチェーンの切り方)も一緒に見ていきましょう 💪\n", + "\n", + "エラーの原因はここです👇\n", + "\n", + "```python\n", + "ids = views.loc[mask, \"author_id\"].unique()\n", + "ids.sort() # ← ここで AttributeError\n", + "```\n", + "\n", + "`unique()` の戻り値が **NumPy 配列ではなく `IntegerArray`(pandas の拡張配列)** になっていて、\n", + "この `IntegerArray` には `.sort()` メソッドがないため `AttributeError` が出ています。\n", + "\n", + "pandas 2 系では、`Int64`(nullable int)などの拡張 dtype だと\n", + "\n", + "* `Series.unique()` → `IntegerArray` を返す\n", + "\n", + "という挙動になるので、私の前回の「`unique()` して `.sort()`」案がそのままだと壊れます。\n", + "\n", + "---\n", + "\n", + "## 対応方法①(おすすめ:NumPy の `unique` を使う)\n", + "\n", + "NumPy の `np.unique` は\n", + "\n", + "* ソート済みのユニーク値を返してくれる\n", + "* 戻り値は NumPy 配列(`ndarray`)なので、そのまま DataFrame にできる\n", + "\n", + "ので、**そもそも `.sort()` を呼ばずに済ませる**のが一番シンプルです。\n", + "\n", + "```python\n", + "import pandas as pd\n", + "import numpy as np\n", + "\n", + "def article_views(views: pd.DataFrame) -> pd.DataFrame:\n", + " \"\"\"\n", + " Returns:\n", + " pd.DataFrame: 列名と順序は ['id']\n", + " \"\"\"\n", + " # 著者 = 閲覧者 の行だけブール配列で抽出\n", + " mask = views[\"author_id\"].values == views[\"viewer_id\"].values\n", + "\n", + " # 対象行の author_id を取り出し、NaN を除外して numpy 配列化\n", + " s = views.loc[mask, \"author_id\"].dropna()\n", + "\n", + " # ユニークかつ昇順の ID を取得(np.unique はソート済みで返す)\n", + " ids = np.unique(s.to_numpy())\n", + "\n", + " # 仕様どおり id 列だけの DataFrame にして返す\n", + " return pd.DataFrame({\"id\": ids})\n", + "\n", + "Analyze Complexity\n", + "Runtime 282 ms\n", + "Beats 58.31%\n", + "Memory 66.98 MB\n", + "Beats 92.43%\n", + "\n", + "```\n", + "\n", + "ポイント:\n", + "\n", + "* `dropna()` しておくことで、もし `author_id` に `` が混じっていても安全\n", + "* `np.unique` は「ユニーク+昇順ソート」を一度でやってくれる\n", + "* `sort_values` は使っていないので、問題の制約も満たす\n", + "\n", + "---\n", + "\n", + "## 対応方法②(NumPyを使わない純Python版)\n", + "\n", + "もし「NumPy使いたくない/使えない」ケースがあれば、\n", + "`set` + `sorted` で同じことができます。\n", + "\n", + "```python\n", + "import pandas as pd\n", + "\n", + "def article_views(views: pd.DataFrame) -> pd.DataFrame:\n", + " mask = views[\"author_id\"].values == views[\"viewer_id\"].values\n", + "\n", + " s = views.loc[mask, \"author_id\"].dropna()\n", + "\n", + " # set でユニーク化し、sorted で昇順リストに\n", + " ids = sorted(set(s.tolist()))\n", + "\n", + " return pd.DataFrame({\"id\": ids})\n", + "\n", + "Analyze Complexity\n", + "Runtime 262 ms\n", + "Beats 92.26%\n", + "Memory 66.62 MB\n", + "Beats 98.51%\n", + "\n", + "```\n", + "\n", + "こちらも\n", + "\n", + "* 中間 DataFrame を作らない\n", + "* `sort_values` は使わない\n", + "* ユニーク+昇順を満たす\n", + "\n", + "という点で要件を満たします。\n", + "\n", + "---\n", + "\n", + "## まとめ\n", + "\n", + "* エラーの直接原因は\n", + " **`IntegerArray`(拡張配列)には `.sort()` がない**のに呼んでしまったこと。\n", + "* 対応としては\n", + "\n", + " * `np.unique` を使って「ユニーク+ソート」を一発でやる\n", + " * もしくは `set` + `sorted` で純 Python でやる\n", + "\n", + "のどちらかに変えれば解決します。\n", + "\n", + "上のどちらかに置き換えて再実行してみてください。\n", + "もしまた別のエラーや Runtime / Memory の数字が出たら、それを見ながらさらに微調整も一緒にやりましょう 💪\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "35b29204", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/SQL/Leetcode/Basic select/1148. Article Views I/chatGPT 5.1 thinking customized/Article_Views_I_postgre.ipynb b/SQL/Leetcode/Basic select/1148. Article Views I/chatGPT 5.1 thinking customized/Article_Views_I_postgre.ipynb new file mode 100644 index 00000000..8c7a26e7 --- /dev/null +++ b/SQL/Leetcode/Basic select/1148. Article Views I/chatGPT 5.1 thinking customized/Article_Views_I_postgre.ipynb @@ -0,0 +1,298 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "35b29204", + "metadata": {}, + "source": [ + "## 0) 前提\n", + "\n", + "* エンジン: **PostgreSQL 16.6+**\n", + "* 並び順: `id` 昇順(問題指定)\n", + "* `NOT IN` は本問題では不要(単純条件で完結)\n", + "* 判定は **「著者が自分の記事を閲覧したか」= `author_id = viewer_id`** 基準\n", + " 表示は列名 `id` で返す\n", + "\n", + "---\n", + "\n", + "## 1) 問題\n", + "\n", + "* `{{PROBLEM_STATEMENT}}`\n", + " 著者が自分で書いた記事を **少なくとも 1 回** 閲覧した著者の ID を求め、\n", + " 列名 `id` で一意にして昇順で返す。\n", + "\n", + "* 入力: `{{TABLES_OR_SCHEMAS}}`\n", + "\n", + " ```text\n", + " Table: Views\n", + "\n", + " +-------------+---------+\n", + " | Column Name | Type |\n", + " +-------------+---------+\n", + " | article_id | int |\n", + " | author_id | int |\n", + " | viewer_id | int |\n", + " | view_date | date |\n", + " +-------------+---------+\n", + "\n", + " ・PK は定義されておらず、行は重複しうる。\n", + " ・各行は「ある viewer が、ある author の記事を、ある日に閲覧した」ことを表す。\n", + " ・`author_id = viewer_id` のとき「著者本人が自分の記事を閲覧した」ことを意味する。\n", + " ```\n", + "\n", + "* 出力: `{{OUTPUT_COLUMNS_AND_RULES}}`\n", + "\n", + " ```text\n", + " +------+\n", + " | id |\n", + " +------+\n", + " | ... |\n", + " +------+\n", + "\n", + " ・自分の記事を 1 回以上閲覧した著者の ID を重複なく返す。\n", + " ・列名は必ず `id`。\n", + " ・`id` 昇順でソート。\n", + " ```\n", + "\n", + "---\n", + "\n", + "## 2) 最適解(単一クエリ)\n", + "\n", + "> この問題は CTE やウィンドウ関数を使うまでもないため、\n", + "> **シンプルな `SELECT DISTINCT`** で解決するのが最適です。\n", + "\n", + "```sql\n", + "SELECT DISTINCT\n", + " author_id AS id\n", + "FROM Views v\n", + "WHERE v.author_id = v.viewer_id\n", + "ORDER BY id;\n", + "\n", + "Runtime 337 ms\n", + "Beats 80.65%\n", + "\n", + "```\n", + "\n", + "### 補足:`GROUP BY` 版(等価解)\n", + "\n", + "```sql\n", + "SELECT\n", + " author_id AS id\n", + "FROM Views v\n", + "WHERE v.author_id = v.viewer_id\n", + "GROUP BY author_id\n", + "ORDER BY id;\n", + "\n", + "Runtime 339 ms\n", + "Beats 75.65%\n", + "\n", + "```\n", + "\n", + "---\n", + "\n", + "## 3) 要点解説\n", + "\n", + "1. **「自分の記事を閲覧した行」の抽出**\n", + "\n", + " ```sql\n", + " WHERE v.author_id = v.viewer_id\n", + " ```\n", + "\n", + " * 著者と閲覧者が同一人物の行だけを残す。\n", + " * 問題文の「自分の記事を閲覧した」をそのまま条件にした形。\n", + "\n", + "2. **著者単位に一意化**\n", + "\n", + " ```sql\n", + " SELECT DISTINCT author_id AS id\n", + " ```\n", + "\n", + " * 同じ著者が同じ記事を複数回見ていても **1 行だけ** にする必要がある。\n", + " * `DISTINCT`(または `GROUP BY author_id`)で **著者 ID をユニーク化**。\n", + "\n", + "3. **出力列名と順序の揃え**\n", + "\n", + " ```sql\n", + " author_id AS id\n", + " ORDER BY id;\n", + " ```\n", + "\n", + " * LeetCode の仕様に合わせ、列名は必ず `id` にリネーム。\n", + " * ソート順も問題指定どおり `id` 昇順。\n", + "\n", + "4. **`NOT IN` 回避について**\n", + "\n", + " * 今回は「除外」ではなく「該当する行の抽出」なので `NOT IN` 自体が不要。\n", + " * そのため、`EXISTS` や `LEFT JOIN ... IS NULL` のようなパターンも出番なし。\n", + " * 将来の類題では「A だが B ではない」のようなときに `NOT IN` の代わりに `NOT EXISTS` を使うとよい。\n", + "\n", + "---\n", + "\n", + "## 4) 計算量(概算)\n", + "\n", + "* `Views` の行数を **N**、条件を満たす著者数を **A** とすると:\n", + "\n", + "1. **WHERE 句のフィルタ (`author_id = viewer_id`)**\n", + "\n", + " * 全行スキャンで **O(N)**\n", + " * インデックスがあれば `O(log N)` スキャン+部分探索に近づくが、ここでは概ね線形とみなす。\n", + "\n", + "2. **`DISTINCT` or `GROUP BY` による重複排除**\n", + "\n", + " * 著者 ID のユニーク化はソートベースの場合おおむね **O(A log A)**。\n", + " * 著者数が全体よりかなり少ない場合、コストは小さく抑えられる。\n", + "\n", + "3. **インデックスのヒント**\n", + "\n", + " * 実務なら以下のような複合インデックスを貼るとよいケースが多い:\n", + "\n", + " ```sql\n", + " CREATE INDEX ON Views (author_id, viewer_id);\n", + " ```\n", + "\n", + " * これにより `WHERE author_id = viewer_id` の探索が効率化される可能性がある。\n", + "\n", + "---\n", + "\n", + "## 5) 図解(Mermaid 超保守版)\n", + "\n", + "```mermaid\n", + "flowchart TD\n", + " A[Views テーブル 全閲覧ログ] --> B[著者 = 閲覧者\n", + " の行だけ \n", + " WHERE author_id = viewer_id]\n", + " B --> C[author_id ごとに一意化 \n", + " DISTINCT / GROUP BY]\n", + " C --> D[列名 id にリネーム \n", + " author_id AS id]\n", + " D --> E[ORDER BY id ASC]\n", + " E --> F[\"出力: 著者IDリスト\n", + " (id 列のみ)\"]\n", + "```\n", + "\n", + "このクエリをそのまま PostgreSQL 16.6+ に流し込めば、問題の入出力仕様を満たす結果が得られます。\n", + "\n", + "結論から言うと、そのクエリはこの問題に対して**ほぼ最適**で、SQL レベルでこれ以上大きく速くする余地はほとんどありません 👍\n", + "\n", + "---\n", + "\n", + "## 1. DISTINCT 版 vs GROUP BY 版について\n", + "\n", + "```sql\n", + "SELECT DISTINCT\n", + " author_id AS id\n", + "FROM Views v\n", + "WHERE v.author_id = v.viewer_id\n", + "ORDER BY id;\n", + "```\n", + "\n", + "```sql\n", + "SELECT\n", + " author_id AS id\n", + "FROM Views v\n", + "WHERE v.author_id = v.viewer_id\n", + "GROUP BY author_id\n", + "ORDER BY id;\n", + "```\n", + "\n", + "PostgreSQL のプランナー的には、この2つはほぼ同じ実行計画になります。\n", + "\n", + "* `DISTINCT` → 内部的に `GROUP BY` 相当の処理(`HashAggregate` or `Sort + Unique`)\n", + "* `GROUP BY author_id` → そのまま集約\n", + "\n", + "LeetCode の実行環境の誤差(他ユーザーの並列実行状況など)を考えると、337ms vs 339ms は**完全に誤差レベル**です。\n", + "「Beats X%」の差も、ほぼ運ゲーみたいなものと考えてOKです。\n", + "\n", + "---\n", + "\n", + "## 2. ロジック面での改善余地\n", + "\n", + "ロジックとしてはすでにミニマルで、これ以上削るところがありません。\n", + "\n", + "* 条件は本質そのもの:`author_id = viewer_id`\n", + "* 余計な列もなく:`author_id` のみ\n", + "* 重複排除も最小限:`DISTINCT` / `GROUP BY`\n", + "\n", + "この問題に対して **JOIN もサブクエリも不要** なので、今の形が「教科書的な模範解答」です。\n", + "\n", + "---\n", + "\n", + "## 3. 実務 PostgreSQL でのチューニング余地(LeetCode 以外の話)\n", + "\n", + "LeetCode 環境では DDL(INDEX 作成など)はできない前提なので、**もし実務で巨大テーブルに対して同じクエリを投げるなら**という観点での改善案を挙げておきます。\n", + "\n", + "### (1) インデックス追加\n", + "\n", + "```sql\n", + "CREATE INDEX CONCURRENTLY idx_views_author_viewer\n", + " ON Views (author_id, viewer_id);\n", + "```\n", + "\n", + "* `WHERE author_id = viewer_id` 条件での探索がかなり速くなる可能性があります。\n", + "* 条件に完全一致する行が少ないなら、インデックススキャン+ビットマップヒープスキャンなどが効きやすくなります。\n", + "\n", + "もっと尖らせるなら、部分インデックスも検討できます:\n", + "\n", + "```sql\n", + "CREATE INDEX CONCURRENTLY idx_views_self_view\n", + " ON Views (author_id)\n", + " WHERE author_id = viewer_id;\n", + "```\n", + "\n", + "* 「自分の記事を見た行だけ」をインデックスに持つので、対象行が少ないほど効きやすいです。\n", + "\n", + "### (2) 統計情報と ANALYZE\n", + "\n", + "巨大テーブルなら、`ANALYZE Views;` で統計情報を最新化しておくことで\n", + "\n", + "* プランナーが「`author_id = viewer_id` を満たす行の割合」を適切に見積もれる\n", + "* 結果として Seq Scan / Index Scan / Bitmap Scan の選択が賢くなる\n", + "\n", + "…といったプラン最適化が期待できます。\n", + "\n", + "---\n", + "\n", + "## 4. 書き方の微修正(ほぼ好み)\n", + "\n", + "今の書き方も十分読みやすいですが、個人的には LeetCode のような単一テーブル問題なら、もう少しだけミニマルにしても良いかな、くらいです。\n", + "\n", + "```sql\n", + "SELECT DISTINCT\n", + " author_id AS id\n", + "FROM Views\n", + "WHERE author_id = viewer_id\n", + "ORDER BY id;\n", + "\n", + "Runtime 330 ms\n", + "Beats 95.81%\n", + "\n", + "```\n", + "\n", + "* テーブルエイリアス(`v`)は使わなくても混乱しないので省略可\n", + "* 実行計画・パフォーマンスは変わりません(完全にスタイルの話)\n", + "\n", + "---\n", + "\n", + "## 5. まとめ\n", + "\n", + "* あなたのクエリは **ロジック的にもパフォーマンス的にもベストクラス** です ✅\n", + "* `DISTINCT` 版と `GROUP BY` 版の差はほぼなく、337ms vs 339ms の違いは誤差と考えてOK。\n", + "* もし実務でテーブルが数千万行以上あるようなケースなら、\n", + "\n", + " * `(author_id, viewer_id)` の複合インデックス\n", + " * or `WHERE author_id = viewer_id` の部分インデックス\n", + " * + `ANALYZE`\n", + " といったチューニングが効いてきます。\n", + "\n", + "この問題に関しては、**SQL の書き方をいじってもほぼこれ以上は速くならない**領域に入っているので、「もう十分やり切った」と思って大丈夫です 🙆‍♂️\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}