Skip to content

Commit 55f26e0

Browse files
committed
leetcode 177. Nth Highest Salary SQL
1 parent 5b4171d commit 55f26e0

3 files changed

Lines changed: 1238 additions & 0 deletions

File tree

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
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

Comments
 (0)