|
| 1 | +{ |
| 2 | + "cells": [ |
| 3 | + { |
| 4 | + "cell_type": "markdown", |
| 5 | + "id": "a12bd06c", |
| 6 | + "metadata": {}, |
| 7 | + "source": [ |
| 8 | + "# MySQL 8.0.40\n", |
| 9 | + "\n", |
| 10 | + "## 0) 前提\n", |
| 11 | + "\n", |
| 12 | + "* エンジン: **MySQL 8**\n", |
| 13 | + "* 並び順: 任意(`ORDER BY` を付けない)\n", |
| 14 | + "* `NOT IN` は NULL 罠のため回避\n", |
| 15 | + "* 判定は **ID 基準**、表示は仕様どおりの列名と順序\n", |
| 16 | + "\n", |
| 17 | + "## 1) 問題\n", |
| 18 | + "\n", |
| 19 | + "* `MyNumbers` から **ちょうど1回だけ出現する数(single number)**のうち **最大の数**を1行で返す。存在しなければ `null` を返す。\n", |
| 20 | + "\n", |
| 21 | + "* 入力テーブル例:\n", |
| 22 | + "\n", |
| 23 | + " ```\n", |
| 24 | + " Table: MyNumbers\n", |
| 25 | + " +-------------+------+\n", |
| 26 | + " | Column Name | Type |\n", |
| 27 | + " +-------------+------+\n", |
| 28 | + " | num | int |\n", |
| 29 | + " +-------------+------+\n", |
| 30 | + " -- 重複あり得る\n", |
| 31 | + " ```\n", |
| 32 | + "\n", |
| 33 | + "* 出力仕様:\n", |
| 34 | + "\n", |
| 35 | + " ```\n", |
| 36 | + " +-----+\n", |
| 37 | + " | num |\n", |
| 38 | + " +-----+\n", |
| 39 | + " | 6 | -- single number の最大。存在しなければ NULL\n", |
| 40 | + " +-----+\n", |
| 41 | + " ```\n", |
| 42 | + "\n", |
| 43 | + "## 2) 最適解(単一クエリ)\n", |
| 44 | + "\n", |
| 45 | + "> ウィンドウ関数で「出現回数」を各行に載せ、そのうち `cnt = 1` の `num` の **最大値**を投影。\n", |
| 46 | + "\n", |
| 47 | + "```sql\n", |
| 48 | + "WITH win AS (\n", |
| 49 | + " SELECT\n", |
| 50 | + " num,\n", |
| 51 | + " COUNT(*) OVER (PARTITION BY num) AS cnt\n", |
| 52 | + " FROM MyNumbers\n", |
| 53 | + ")\n", |
| 54 | + "SELECT\n", |
| 55 | + " MAX(num) AS num\n", |
| 56 | + "FROM win\n", |
| 57 | + "WHERE cnt = 1;\n", |
| 58 | + "\n", |
| 59 | + "Runtime 392 ms\n", |
| 60 | + "Beats 64.64%\n", |
| 61 | + "\n", |
| 62 | + "```\n", |
| 63 | + "\n", |
| 64 | + "* `MAX(num)` により **並び替え不要**で最大の single number を1行で取得\n", |
| 65 | + "* single number が存在しない場合、`MAX` の母集団が空になり **`NULL` を返す**(要件どおり)\n", |
| 66 | + "\n", |
| 67 | + "## 3) 代替解\n", |
| 68 | + "\n", |
| 69 | + "> 集約のみで十分なサイズなら、`GROUP BY ... HAVING` → その最大値を返す。\n", |
| 70 | + "\n", |
| 71 | + "```sql\n", |
| 72 | + "SELECT\n", |
| 73 | + " MAX(num) AS num\n", |
| 74 | + "FROM (\n", |
| 75 | + " SELECT num\n", |
| 76 | + " FROM MyNumbers\n", |
| 77 | + " GROUP BY num\n", |
| 78 | + " HAVING COUNT(*) = 1\n", |
| 79 | + ") s;\n", |
| 80 | + "\n", |
| 81 | + "Runtime 418 ms\n", |
| 82 | + "Beats 39.99%\n", |
| 83 | + "\n", |
| 84 | + "```\n", |
| 85 | + "\n", |
| 86 | + "* `NOT IN` 不要、`NULL` でも安全\n", |
| 87 | + "* インデックスがないと全表スキャンになる点はウィンドウ版と同様\n", |
| 88 | + "\n", |
| 89 | + "## 4) 要点解説\n", |
| 90 | + "\n", |
| 91 | + "* **方針**:\n", |
| 92 | + "\n", |
| 93 | + " 1. 各 `num` の出現回数を計算(ウィンドウ or 集約)\n", |
| 94 | + " 2. `= 1`(single)に絞る\n", |
| 95 | + " 3. **最大値**だけを返す → `ORDER BY`・`LIMIT` 不要\n", |
| 96 | + "* **NULL / 重複**:\n", |
| 97 | + "\n", |
| 98 | + " * `num` に `NULL` があっても `COUNT(*)` は `NULL` を数えるため、`num IS NULL` は single になり得る。ただし問題の意図は整数なので通常は非NULL前提。もし `NULL` 行があっても `MAX(num)` は `NULL` を無視するため影響しない。\n", |
| 99 | + "* **安定性**:\n", |
| 100 | + "\n", |
| 101 | + " * 出力は1行のみで順序不要。`ORDER BY` を付けないほうが速い。\n", |
| 102 | + "\n", |
| 103 | + "## 5) 計算量(概算)\n", |
| 104 | + "\n", |
| 105 | + "* ウィンドウ版: `COUNT() OVER (PARTITION BY num)` は **O(N)**~**O(N log N)**(実装依存・ソート/ハッシュ)\n", |
| 106 | + "* 集約版: `GROUP BY num` は **O(N)**~**O(N log N)**\n", |
| 107 | + "* 推奨インデックス: `INDEX(num)` があればハッシュ/ツリー集約が効きやすい\n", |
| 108 | + "\n", |
| 109 | + "## 6) 図解(Mermaid 超保守版)\n", |
| 110 | + "\n", |
| 111 | + "```mermaid\n", |
| 112 | + "flowchart TD\n", |
| 113 | + " A[入力 MyNumbers] --> B[出現回数を算出 cnt]\n", |
| 114 | + " B --> C[cnt が 1 の行に絞る]\n", |
| 115 | + " C --> D[最大 num を求める]\n", |
| 116 | + " D --> E[出力 列 num だけ]\n", |
| 117 | + "```\n", |
| 118 | + "\n", |
| 119 | + "いい感じの結果です(特にウィンドウ版で ~65% 上回り)が、**もう少し縮められる余地**はあります。要点だけ手短に👇\n", |
| 120 | + "\n", |
| 121 | + "---\n", |
| 122 | + "\n", |
| 123 | + "## まずはインデックス\n", |
| 124 | + "\n", |
| 125 | + "```sql\n", |
| 126 | + "CREATE INDEX ix_mynumbers_num ON MyNumbers(num);\n", |
| 127 | + "```\n", |
| 128 | + "\n", |
| 129 | + "* `GROUP BY num` / `COUNT(*)` が**インデックス順走査**でまとまりやすくなり、\n", |
| 130 | + " 一時テーブルやファイルソートの発生を抑制できます(環境次第で体感差が大きいところ)。\n", |
| 131 | + "\n", |
| 132 | + "---\n", |
| 133 | + "\n", |
| 134 | + "## 速度重視の実戦解(早期終了を効かせる)\n", |
| 135 | + "\n", |
| 136 | + "> 並び順が任意という仕様でしたが、**パフォーマンス最優先**なら `ORDER BY ... DESC LIMIT 1` による **早期終了**が効きます。`INDEX(num)` があると特に強いです。\n", |
| 137 | + "\n", |
| 138 | + "```sql\n", |
| 139 | + "-- 早いことが多い版(上位1件だけ取りに行く)\n", |
| 140 | + "SELECT num\n", |
| 141 | + "FROM MyNumbers\n", |
| 142 | + "GROUP BY num\n", |
| 143 | + "HAVING COUNT(*) = 1\n", |
| 144 | + "ORDER BY num DESC\n", |
| 145 | + "LIMIT 1;\n", |
| 146 | + "\n", |
| 147 | + "Wrong Answer\n", |
| 148 | + "13 / 18 testcases passed\n", |
| 149 | + "```\n", |
| 150 | + "\n", |
| 151 | + "* 右端(最大値側)から**逆順インデックス走査**し、最初に見つかった「出現1回」のグループで終わるため、\n", |
| 152 | + " データ分布によっては **大幅短縮**します(特に「大きい値ほどユニークが出やすい」分布)。\n", |
| 153 | + "\n", |
| 154 | + "> 出力は1行だけで、外側に `MAX` をかける必要はありません。\n", |
| 155 | + "\n", |
| 156 | + "---\n", |
| 157 | + "\n", |
| 158 | + "## `ORDER BY` を避けたい場合の最適形\n", |
| 159 | + "\n", |
| 160 | + "あなたの代替解は正攻法で、インデックス追加だけでも十分効きます。書式はそのままでOK:\n", |
| 161 | + "\n", |
| 162 | + "```sql\n", |
| 163 | + "-- あなたの代替解(INDEXあり想定)\n", |
| 164 | + "SELECT\n", |
| 165 | + " MAX(num) AS num\n", |
| 166 | + "FROM (\n", |
| 167 | + " SELECT num\n", |
| 168 | + " FROM MyNumbers\n", |
| 169 | + " GROUP BY num\n", |
| 170 | + " HAVING COUNT(*) = 1\n", |
| 171 | + ") s;\n", |
| 172 | + "\n", |
| 173 | + "Runtime 396 ms\n", |
| 174 | + "Beats 60.49%\n", |
| 175 | + "\n", |
| 176 | + "```\n", |
| 177 | + "\n", |
| 178 | + "* ウィンドウ版より **`GROUP BY` 直集約**のほうが MySQL では速く出ることが多いです(特に `INDEX(num)` あり)。\n", |
| 179 | + "\n", |
| 180 | + "---\n", |
| 181 | + "\n", |
| 182 | + "## 重複が極端に多い場合の小技(重複集合を先に作る)\n", |
| 183 | + "\n", |
| 184 | + "> 「ほとんどが重複で、ユニークが少ない」分布なら、**重複集合だけ先に抽出**して引き算すると速いことがあります。\n", |
| 185 | + "\n", |
| 186 | + "```sql\n", |
| 187 | + "WITH dup AS (\n", |
| 188 | + " SELECT num\n", |
| 189 | + " FROM MyNumbers\n", |
| 190 | + " GROUP BY num\n", |
| 191 | + " HAVING COUNT(*) > 1\n", |
| 192 | + "),\n", |
| 193 | + "uniq AS (\n", |
| 194 | + " SELECT DISTINCT num\n", |
| 195 | + " FROM MyNumbers\n", |
| 196 | + " LEFT JOIN dup USING (num)\n", |
| 197 | + " WHERE dup.num IS NULL\n", |
| 198 | + ")\n", |
| 199 | + "SELECT MAX(num) AS num\n", |
| 200 | + "FROM uniq;\n", |
| 201 | + "\n", |
| 202 | + "Runtime 414 ms\n", |
| 203 | + "Beats 43.42%\n", |
| 204 | + "\n", |
| 205 | + "```\n", |
| 206 | + "\n", |
| 207 | + "* `dup` のサイズが小さくなる分、以降の結合・探索が軽くなります(分布依存)。\n", |
| 208 | + "\n", |
| 209 | + "---\n", |
| 210 | + "\n", |
| 211 | + "## 実務メモ\n", |
| 212 | + "\n", |
| 213 | + "* `EXPLAIN ANALYZE` で\n", |
| 214 | + " *「Using index」でグループ化できているか*、\n", |
| 215 | + " *一時テーブル/ファイルソートが消えているか* を確認。\n", |
| 216 | + "* `ANALYZE TABLE MyNumbers;` で統計を更新しておくとプランが安定。\n", |
| 217 | + "* `num` に `NULL` が混じっていても、`GROUP BY` + `HAVING COUNT(*)=1` は正しく動き、\n", |
| 218 | + " `MAX(num)` も `NULL` を無視するので問題ありません。\n", |
| 219 | + "\n", |
| 220 | + "---\n", |
| 221 | + "\n", |
| 222 | + "### まとめ\n", |
| 223 | + "\n", |
| 224 | + "* まずは `INDEX(num)` を追加。\n", |
| 225 | + "* 速度をさらに取りに行くなら **`GROUP BY ... HAVING COUNT(*)=1 ORDER BY num DESC LIMIT 1`**。\n", |
| 226 | + "* `ORDER BY` を使わない方針を守るなら、あなたの **集約版 + インデックス** が最善に近いです。\n", |
| 227 | + "\n" |
| 228 | + ] |
| 229 | + } |
| 230 | + ], |
| 231 | + "metadata": { |
| 232 | + "language_info": { |
| 233 | + "name": "python" |
| 234 | + } |
| 235 | + }, |
| 236 | + "nbformat": 4, |
| 237 | + "nbformat_minor": 5 |
| 238 | +} |
0 commit comments