|
| 1 | +# ✅ 解法(MySQL 8+, LeetCode 互換関数) |
| 2 | + |
| 3 | +最少・最速の王道は「重複を除いた給与を降順にし、`OFFSET N-1` を 1 件だけ取得」。該当が無い場合、**スカラサブクエリは `NULL` を返す**ので、そのまま要件を満たします。 |
| 4 | + |
| 5 | +```sql |
| 6 | +-- LeetCodeのデータベース上でそのまま通る想定 |
| 7 | +CREATE FUNCTION getNthHighestSalary(N INT) RETURNS INT |
| 8 | +BEGIN |
| 9 | + RETURN ( |
| 10 | + SELECT DISTINCT salary |
| 11 | + FROM Employee |
| 12 | + ORDER BY salary DESC |
| 13 | + LIMIT 1 OFFSET N - 1 |
| 14 | + ); |
| 15 | +END; |
| 16 | +``` |
| 17 | + |
| 18 | +## ローカル(MySQL CLI)で関数を作る場合の書式 |
| 19 | + |
| 20 | +```sql |
| 21 | +DELIMITER $$ |
| 22 | + |
| 23 | +CREATE FUNCTION getNthHighestSalary(N INT) RETURNS INT |
| 24 | +DETERMINISTIC |
| 25 | +READS SQL DATA |
| 26 | +BEGIN |
| 27 | + RETURN ( |
| 28 | + SELECT DISTINCT salary |
| 29 | + FROM Employee |
| 30 | + ORDER BY salary DESC |
| 31 | + LIMIT 1 OFFSET N - 1 |
| 32 | + ); |
| 33 | +END$$ |
| 34 | + |
| 35 | +DELIMITER ; |
| 36 | +``` |
| 37 | + |
| 38 | +### 単発クエリ版(関数を作らず結果だけ見たい時) |
| 39 | + |
| 40 | +```sql |
| 41 | +-- 例: N=2 のとき |
| 42 | +SELECT ( |
| 43 | + SELECT DISTINCT salary |
| 44 | + FROM Employee |
| 45 | + ORDER BY salary DESC |
| 46 | + LIMIT 1 OFFSET 2 - 1 |
| 47 | +) AS `getNthHighestSalary(2)`; |
| 48 | +``` |
| 49 | + |
| 50 | +--- |
| 51 | + |
| 52 | +## 🧠 アルゴリズムの考え方 |
| 53 | + |
| 54 | +1. `DISTINCT salary` で**重複を排除** |
| 55 | +2. `ORDER BY salary DESC` で**高い順に並べ替え** |
| 56 | +3. `OFFSET N-1` までスキップし、`LIMIT 1` を**1 件だけ取り出す** |
| 57 | +4. 件数不足なら**空集合**→ スカラサブクエリは **`NULL`** を返す |
| 58 | + |
| 59 | +**計算量**: |
| 60 | + |
| 61 | +- D = distinct な給与の個数 |
| 62 | +- 並べ替えが支配的 → **O(D log D)** |
| 63 | + |
| 64 | +--- |
| 65 | + |
| 66 | +## 🧩 図解 1:全体フロー |
| 67 | + |
| 68 | +```mermaid |
| 69 | +flowchart LR |
| 70 | + A[Employee table] --> B[Select DISTINCT salary] |
| 71 | + B --> C[ORDER BY salary DESC] |
| 72 | + C --> D[OFFSET N-1] |
| 73 | + D --> E[LIMIT 1] |
| 74 | + E --> F[Return salary or NULL] |
| 75 | +``` |
| 76 | + |
| 77 | +--- |
| 78 | + |
| 79 | +## 🔎 図解 2:Example 1 の流れ(N=2) |
| 80 | + |
| 81 | +入力: |
| 82 | + |
| 83 | +```text |
| 84 | +id | salary |
| 85 | +1 | 100 |
| 86 | +2 | 200 |
| 87 | +3 | 300 |
| 88 | +``` |
| 89 | + |
| 90 | +```mermaid |
| 91 | +flowchart LR |
| 92 | + S1[Input salaries: 100,200,300; N=2] --> D1[Distinct: 100,200,300] |
| 93 | + D1 --> O1[Order DESC: 300,200,100] |
| 94 | + O1 --> K1[Offset 1] |
| 95 | + K1 --> T1[Take 1: 200] |
| 96 | + T1 --> R1[Result: 200] |
| 97 | +``` |
| 98 | + |
| 99 | +--- |
| 100 | + |
| 101 | +## 🟦 図解 3:Example 2 の流れ(N=2, データ不足) |
| 102 | + |
| 103 | +入力: |
| 104 | + |
| 105 | +```text |
| 106 | +id | salary |
| 107 | +1 | 100 |
| 108 | +``` |
| 109 | + |
| 110 | +```mermaid |
| 111 | +flowchart LR |
| 112 | + S2[Input salaries: 100; N=2] --> D2[Distinct: 100] |
| 113 | + D2 --> O2[Order DESC: 100] |
| 114 | + O2 --> K2[Offset 1] |
| 115 | + K2 --> T2[Take 1: none] |
| 116 | + T2 --> R2[Result: NULL] |
| 117 | +``` |
| 118 | + |
| 119 | +--- |
| 120 | + |
| 121 | +## 🧭 代替解(参考):ウィンドウ関数 `DENSE_RANK` |
| 122 | + |
| 123 | +MySQL 8+ なら `DENSE_RANK()` でも書けます(**重複を自然にまとめる**)。 |
| 124 | +ただし関数の `RETURN` に載せるには**スカラ化**が必要なので、`LIMIT 1` などで 1 行に絞ります。 |
| 125 | + |
| 126 | +```sql |
| 127 | +-- DISTINCT→DENSE_RANK で N 位の給与を抽出 |
| 128 | +CREATE FUNCTION getNthHighestSalary_v2(N INT) RETURNS INT |
| 129 | +BEGIN |
| 130 | + RETURN ( |
| 131 | + SELECT salary |
| 132 | + FROM ( |
| 133 | + SELECT salary, DENSE_RANK() OVER (ORDER BY salary DESC) AS rk |
| 134 | + FROM (SELECT DISTINCT salary FROM Employee) AS d |
| 135 | + ) AS r |
| 136 | + WHERE rk = N |
| 137 | + ORDER BY salary |
| 138 | + LIMIT 1 |
| 139 | + ); |
| 140 | +END; |
| 141 | +``` |
| 142 | + |
| 143 | +> どちらも要件を満たしますが、**簡潔で早いのは LIMIT/OFFSET 版**です。 |
| 144 | +
|
| 145 | +--- |
| 146 | + |
| 147 | +## ✅ 動作確認クエリ |
| 148 | + |
| 149 | +```sql |
| 150 | +-- サンプルデータ |
| 151 | +TRUNCATE TABLE Employee; |
| 152 | +INSERT INTO Employee(id, salary) VALUES (1,100),(2,200),(3,300); |
| 153 | + |
| 154 | +-- 期待: 200 |
| 155 | +SELECT getNthHighestSalary(2) AS `getNthHighestSalary(2)`; |
| 156 | + |
| 157 | +-- データ不足パターン |
| 158 | +TRUNCATE TABLE Employee; |
| 159 | +INSERT INTO Employee(id, salary) VALUES (1,100); |
| 160 | + |
| 161 | +-- 期待: NULL |
| 162 | +SELECT getNthHighestSalary(2) AS `getNthHighestSalary(2)`; |
| 163 | +``` |
| 164 | + |
| 165 | +--- |
| 166 | + |
| 167 | +## 🛠️ Mermaid 図のパースエラー回避のコツ |
| 168 | + |
| 169 | +- ノード内テキストは**1 行・短文**に(`<br/>` は使わない) |
| 170 | +- 日本語や絵文字で崩れる場合は**英数字**に |
| 171 | +- 記号は最小限(カンマやコロンは OK) |
| 172 | + |
| 173 | +--- |
| 174 | + |
| 175 | +エラー原因と対処、それから「LeetCode の SQL 実行環境のバージョン確認」についてまとめます。 |
| 176 | + |
| 177 | +--- |
| 178 | + |
| 179 | +## 1) そのエラーのよくある原因 |
| 180 | + |
| 181 | +**メッセージ**: `... near '- 1 ); END'` |
| 182 | +主に次のどちらかが原因です。 |
| 183 | + |
| 184 | +1. **`DELIMITER` を変えずに `CREATE FUNCTION ...` を実行** |
| 185 | + ローカルの MySQL では、関数本体内の `;` で文が分割されてしまいます。 |
| 186 | + → `DELIMITER $$` を使ってから関数を作成してください(下記コード参照)。 |
| 187 | + |
| 188 | +2. **`LIMIT ... OFFSET N - 1` の「式」が許容されないケース** |
| 189 | + 環境によっては `OFFSET` に**式**(`N - 1`)を直接書くと構文エラーになることがあります。 |
| 190 | + → 先に `SET N = N - 1;` で**変数に代入**してから `OFFSET N` を使うと安定します。 |
| 191 | + (LeetCode の解答投稿でもこの書き方がよく使われます。([leetcode.com][1])) |
| 192 | + |
| 193 | +### ✅ ローカル MySQL で確実に通る版 |
| 194 | + |
| 195 | +```sql |
| 196 | +DELIMITER $$ |
| 197 | + |
| 198 | +CREATE FUNCTION getNthHighestSalary(N INT) RETURNS INT |
| 199 | +DETERMINISTIC |
| 200 | +READS SQL DATA |
| 201 | +BEGIN |
| 202 | + SET N = N - 1; -- ここで式を変数に畳み込む |
| 203 | + RETURN ( |
| 204 | + SELECT DISTINCT salary |
| 205 | + FROM Employee |
| 206 | + ORDER BY salary DESC |
| 207 | + LIMIT 1 OFFSET N |
| 208 | + ); |
| 209 | +END$$ |
| 210 | + |
| 211 | +DELIMITER ; |
| 212 | +``` |
| 213 | + |
| 214 | +> 代替:二引数の `LIMIT` でも OK(`LIMIT offset, row_count`) |
| 215 | +> |
| 216 | +> ```sql |
| 217 | +> ... |
| 218 | +> SET N = N - 1; |
| 219 | +> RETURN ( |
| 220 | +> SELECT DISTINCT salary |
| 221 | +> FROM Employee |
| 222 | +> ORDER BY salary DESC |
| 223 | +> LIMIT N, 1 |
| 224 | +> ); |
| 225 | +> ... |
| 226 | +> ``` |
| 227 | +
|
| 228 | +--- |
| 229 | +
|
| 230 | +## 2) LeetCode 側の「SQL バージョン」を確認するには? |
| 231 | +
|
| 232 | +結論から言うと、**LeetCode は公式に RDB の「厳密なバージョン番号」を公開していません**。問題ページの「MySQL / MS SQL Server / Oracle」など**方言は選べますが**、細かなバージョンは明示されません。 |
| 233 | +一方で、LeetCode の Discuss(公式トピック)や問題解説では **MySQL のウィンドウ関数(`DENSE_RANK()` など)を前提にした解法が案内**されており、 |
| 234 | +**少なくとも MySQL 8+ 相当の機能が使える**ことが読み取れます。([leetcode.com][2]) |
| 235 | +
|
| 236 | +### 現実的な確認手段 |
| 237 | +
|
| 238 | +- **機能ベースで推測**する |
| 239 | + 例)`DENSE_RANK()` が動く → **MySQL 8+ 機能**は使える、と判断。([leetcode.com][2]) |
| 240 | +- **Discuss の公式ポストや解説をチェック** |
| 241 | + LeetCode が配信している Database Primer などで使っている機能を確認。([leetcode.com][2]) |
| 242 | +- **サンドボックスに依存しない書き方に寄せる** |
| 243 | + 例えば今回の問題は、**`DISTINCT + ORDER BY DESC + LIMIT`** でウィンドウ関数を使わずに解けるため、**環境差の影響を最小化**できます(本回答の関数)。([leetcode.com][1]) |
| 244 | +
|
| 245 | +> 備考:LeetCode の採点環境では任意クエリで `SELECT VERSION();` のような“自己診断”を走らせることは基本的にできません(採点は固定スキーマ/固定 IO で比較されるため)。 |
| 246 | +
|
| 247 | +--- |
| 248 | +
|
| 249 | +## 3) LeetCode 提出用(簡潔版) |
| 250 | +
|
| 251 | +LeetCode のエディタにそのまま貼るなら、次の **「先に `N` を更新 → `OFFSET N`」** 版が堅いです。 |
| 252 | +
|
| 253 | +```sql |
| 254 | +CREATE FUNCTION getNthHighestSalary(N INT) RETURNS INT |
| 255 | +BEGIN |
| 256 | + SET N = N - 1; |
| 257 | + RETURN ( |
| 258 | + SELECT DISTINCT salary |
| 259 | + FROM Employee |
| 260 | + ORDER BY salary DESC |
| 261 | + LIMIT 1 OFFSET N |
| 262 | + ); |
| 263 | +END; |
| 264 | +``` |
| 265 | +
|
| 266 | +--- |
| 267 | + |
| 268 | +## 4) それでもダメなときのチェックリスト |
| 269 | + |
| 270 | +- ローカル実行なら **`DELIMITER` 切り替え**を入れているか? |
| 271 | +- `Employee` テーブル名・列名の **大小文字**やタイプミスがないか? |
| 272 | +- 既に同名関数があれば **`DROP FUNCTION getNthHighestSalary;`** 実行後に再作成 |
| 273 | +- それでも通らなければ、`SET N = N - 1;` を **`DECLARE off INT DEFAULT N-1;`** → `OFFSET off` に変えてみる(同趣旨) |
| 274 | + |
| 275 | +--- |
| 276 | + |
| 277 | +[1]: https://leetcode.com/problems/nth-highest-salary/discuss/498281/mysql-offset-by-n-1?utm_source=chatgpt.com "Nth Highest Salary - LeetCode" |
| 278 | +[2]: https://leetcode.com/discuss/post/1600719/database-sql-primer-part-2-window-functi-sm8m/?utm_source=chatgpt.com "Database SQL Primer (Part 2) [ Window Functions ] - Discuss" |
| 279 | + |
| 280 | +原因はシンプルで、**`DELIMITER` は MySQL サーバーの構文じゃなく “クライアント側専用ディレクティブ”** だからです。 |
| 281 | +あなたの環境(LeetCode や一部 GUI ツール)は `DELIMITER` を解釈せず、そのままサーバーに送ってしまうため、 |
| 282 | +サーバーが **`DELIMITER $$` を SQL として解釈 → 構文エラー** になります。 |
| 283 | + |
| 284 | +以下から自分の環境に合う対処を選んでください。 |
| 285 | + |
| 286 | +--- |
| 287 | + |
| 288 | +## 1) LeetCode(最有力)なら:`DELIMITER` 行は**全部削除** |
| 289 | + |
| 290 | +LeetCode では **`DELIMITER` を書かない** で OK です。次をそのまま貼ってください。 |
| 291 | + |
| 292 | +```sql |
| 293 | +CREATE FUNCTION getNthHighestSalary(N INT) RETURNS INT |
| 294 | +BEGIN |
| 295 | + -- OFFSET での式(N-1)が環境により嫌がられる場合があるので、先に畳み込みます |
| 296 | + SET N = N - 1; |
| 297 | + RETURN ( |
| 298 | + SELECT DISTINCT salary |
| 299 | + FROM Employee |
| 300 | + ORDER BY salary DESC |
| 301 | + -- どちらでもOK(好きな方で) |
| 302 | + -- LIMIT 1 OFFSET N |
| 303 | + LIMIT N, 1 |
| 304 | + ); |
| 305 | +END; |
| 306 | +``` |
| 307 | + |
| 308 | +動作確認(LeetCode では提出後に内部で呼ばれますが、ローカルならこう呼べます): |
| 309 | + |
| 310 | +```sql |
| 311 | +SELECT getNthHighestSalary(2); |
| 312 | +``` |
| 313 | + |
| 314 | +--- |
| 315 | + |
| 316 | +## 2) MySQL CLI / MySQL Workbench なら:`DELIMITER` を**別行で**使う |
| 317 | + |
| 318 | +`mysql` CLI や Workbench は `DELIMITER` を理解します。**必ず “別行” で実行**してください(1 行目の頭から `DELIMITER` を置く)。 |
| 319 | + |
| 320 | +```sql |
| 321 | +DELIMITER $$ |
| 322 | + |
| 323 | +CREATE FUNCTION getNthHighestSalary(N INT) RETURNS INT |
| 324 | +DETERMINISTIC |
| 325 | +READS SQL DATA |
| 326 | +BEGIN |
| 327 | + SET N = N - 1; |
| 328 | + RETURN ( |
| 329 | + SELECT DISTINCT salary |
| 330 | + FROM Employee |
| 331 | + ORDER BY salary DESC |
| 332 | + LIMIT N, 1 |
| 333 | + ); |
| 334 | +END$$ |
| 335 | + |
| 336 | +DELIMITER ; |
| 337 | +``` |
| 338 | + |
| 339 | +> ポイント |
| 340 | +> |
| 341 | +> - 「スクリプト全体を一気に実行」すれば OK(Workbench なら稲妻ボタン)。 |
| 342 | +> - `DELIMITER` と `CREATE FUNCTION ... END$$`、`DELIMITER ;` は**各行独立**で送ること。 |
| 343 | +
|
| 344 | +--- |
| 345 | + |
| 346 | +## 3) DataGrip / DBeaver / phpMyAdmin など GUI で `DELIMITER` を解釈しない場合 |
| 347 | + |
| 348 | +- **方法 A(推奨)**: そのツールの「バッチ/スクリプト実行モード」を使う(Workbench と同様に扱えるモードがあることが多い)。 |
| 349 | +- **方法 B**: エディタ設定で「区切り文字(Delimiter)」を `$$` に変更できる UI があれば、そこで設定してから実行。 |
| 350 | +- **方法 C**: 面倒なら **LeetCode 方式(`DELIMITER` 無し)**の関数を貼って実行(多くの GUI は 1 クエリとしてまとめて送るため、そのまま通ります)。 |
| 351 | + |
| 352 | +--- |
| 353 | + |
| 354 | +## まだエラーが出る場合のチェックリスト |
| 355 | + |
| 356 | +- 既に同名関数がある → `DROP FUNCTION getNthHighestSalary;` してから再作成 |
| 357 | +- テーブル名・列名のタイプミス無し?(`Employee` / `salary`) |
| 358 | +- クライアントがステートメントを**1 行ずつ**投げていない?(`CREATE FUNCTION` は**1 リクエストで全文**送る必要があります) |
| 359 | +- 以前の環境では `OFFSET N-1` が NG だった → **二引数 `LIMIT N, 1`** を使う(上記スニペットは対応済み) |
0 commit comments