Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "7d52f65e",
"metadata": {},
"source": [
"# MySQL 8.0.40\n",
"\n",
"## 0) 前提\n",
"\n",
"* エンジン: **MySQL 8**\n",
"* 並び順: 任意(`ORDER BY` を付けない)\n",
" ※この問題は仕様として **`id` 昇順で返す** ことが明記されているため、最終 `ORDER BY id` を付与します\n",
"* `NOT IN` は NULL 罠のため回避\n",
"* 判定は **ID 基準**、表示は仕様どおりの列名と順序\n",
"\n",
"## 1) 問題\n",
"\n",
"* `連番 ID を持つ Seat テーブルで、学生を 2 人ずつで席替え(ID をスワップ)する。人数が奇数なら最後の 1 人はそのまま。結果は id 昇順で返す。`\n",
"* 入力テーブル例: `Seat(id INT PK, student VARCHAR)`\n",
"* 出力仕様: `id, student`\n",
"\n",
" * 奇数 `id` は「次が存在するときだけ」`id+1` にスワップ\n",
" * 偶数 `id` は常に `id-1` にスワップ\n",
" * 最後の孤立した奇数 `id` は据え置き\n",
" * **最終出力は `id` 昇順**\n",
"\n",
"## 2) 最適解(単一クエリ)\n",
"\n",
"> ウィンドウで末尾 ID を取り、`CASE` で ±1 変換。**外側で `ORDER BY id`** を必ず付ける。\n",
"\n",
"```sql\n",
"WITH win AS (\n",
" SELECT id, student, MAX(id) OVER () AS max_id\n",
" FROM Seat\n",
")\n",
"SELECT\n",
" CASE\n",
" WHEN id % 2 = 1 AND id < max_id THEN id + 1 -- 奇数かつ次がある\n",
" WHEN id % 2 = 0 THEN id - 1 -- 偶数は前と交換\n",
" ELSE id -- 最後の孤立奇数\n",
" END AS id,\n",
" student\n",
"FROM win\n",
"ORDER BY id; -- 仕様:id 昇順\n",
"\n",
"Runtime 316 ms\n",
"Beats 70.97%\n",
"\n",
"```\n",
"\n",
"## 3) 代替解\n",
"\n",
"> ウィンドウを避ける場合は、定数サブクエリで最大 ID を 1 回だけ取得(MySQL は定数サブクエリをキャッシュ)。性能と可読性のバランスが良い。\n",
"\n",
"```sql\n",
"SELECT\n",
" CASE\n",
" WHEN s.id % 2 = 1 AND s.id < (SELECT MAX(id) FROM Seat) THEN s.id + 1\n",
" WHEN s.id % 2 = 0 THEN s.id - 1\n",
" ELSE s.id\n",
" END AS id,\n",
" s.student\n",
"FROM Seat AS s\n",
"ORDER BY id; -- 仕様:id 昇順\n",
"\n",
"Runtime 321 ms\n",
"Beats 64.40%\n",
"\n",
"```\n",
"\n",
"## 4) 要点解説\n",
"\n",
"* **誤答の原因**は多くの場合、**`ORDER BY id` を付けていない**こと(計算後の `id` で並べ替えないと期待順にならない)\n",
"* 連番前提なのでスワップは **±1** で表現可能。末尾の奇数だけは「次がない」ため据え置き(`id < max_id` 判定)\n",
"* 余計な自己結合は不要。存在判定は `MAX(id)` を使うのがシンプル\n",
"\n",
"## 5) 計算量(概算)\n",
"\n",
"* ウィンドウ版: スキャン+ウィンドウ伝播で **O(N)**(実装上は並べ替え不要な全体関数なので追加コスト小)\n",
"* 代替版: スキャン **O(N)**、サブクエリ `MAX(id)` は 1 回評価\n",
"\n",
"## 6) 図解(Mermaid 超保守版)\n",
"\n",
"```mermaid\n",
"flowchart TD\n",
" A[Seat 入力] --> B[\"末尾ID MAX(id) を取得\"]\n",
" B --> C[\"CASE で ±1 変換\"]\n",
" C --> D[id 昇順で整列]\n",
" D --> E[出力 id, student]\n",
"```\n",
"\n",
"# MySQL 8.0.40\n",
"\n",
"## 0) 前提\n",
"\n",
"* エンジン: **MySQL 8**\n",
"* 並び順: 任意だが、本問は**`id` 昇順で返す指定**があるため `ORDER BY id` を付ける\n",
"* `NOT IN` は NULL 罠のため回避\n",
"* 判定は **ID 基準**、表示は仕様どおりの列名と順序\n",
"\n",
"## 1) 問題\n",
"\n",
"* `連番 ID を持つ Seat テーブルで、学生を 2 人ずつ席替え(ID スワップ)する。人数が奇数なら最後の 1 人はそのまま。結果は id 昇順で返す。`\n",
"* 入力テーブル例: `Seat(id INT PK, student VARCHAR)`\n",
"* 出力仕様: `id, student`(奇数 ↔ 直後の偶数で入れ替え。末尾の孤立奇数は据え置き)\n",
"\n",
"## 2) 最適解(単一クエリ)\n",
"\n",
"> 実行時間を詰める観点で、**ウィンドウ関数や CTE を使わず**、`MAX(id)` を 1 回だけ求めて全行に結合する「定数サブクエリ JOIN」+**ビット演算**(`& 1`)を使います。\n",
"> (`id % 2` より `id & 1` のほうが軽量。CTE は場合によりマテリアライズされオーバーヘッドになり得ます)\n",
"\n",
"```sql\n",
"SELECT\n",
" CASE\n",
" WHEN (s.id & 1) = 1 AND s.id < m.max_id THEN s.id + 1 -- 奇数かつ次がある\n",
" WHEN (s.id & 1) = 0 THEN s.id - 1 -- 偶数は前と交換\n",
" ELSE s.id -- 最後の孤立奇数\n",
" END AS id,\n",
" s.student\n",
"FROM Seat AS s\n",
"JOIN (SELECT MAX(id) AS max_id FROM Seat) AS m\n",
"ORDER BY id; -- 指定どおり id 昇順\n",
"\n",
"Runtime 345 ms\n",
"Beats 35.97%\n",
"\n",
"```\n",
"\n",
"* ポイント\n",
"\n",
" * `JOIN (SELECT MAX(id) ...)` は最適化され 1 回だけ評価されるため、**スカラーサブクエリを各行で評価**する形より安定しやすいです。\n",
" * `& 1` による偶奇判定は `MOD(id,2)` / `id % 2` より安価です。\n",
"\n",
"## 3) 代替解\n",
"\n",
"> 「分岐評価自体を減らす」アプローチ。対象行を 3 つに分解して `UNION ALL`(それぞれに最小限の述語だけを評価)。環境次第で速くなることがあります。\n",
"\n",
"```sql\n",
"SELECT s.id + 1 AS id, s.student -- 奇数で次がある\n",
"FROM Seat AS s\n",
"JOIN (SELECT MAX(id) AS max_id FROM Seat) AS m\n",
"WHERE (s.id & 1) = 1 AND s.id < m.max_id\n",
"\n",
"UNION ALL\n",
"SELECT s.id - 1, s.student -- 偶数\n",
"FROM Seat AS s\n",
"WHERE (s.id & 1) = 0\n",
"\n",
"UNION ALL\n",
"SELECT s.id, s.student -- 最後の孤立奇数\n",
"FROM Seat AS s\n",
"JOIN (SELECT MAX(id) AS max_id FROM Seat) AS m\n",
"WHERE (s.id & 1) = 1 AND s.id = m.max_id\n",
"\n",
"ORDER BY id; -- 指定どおり id 昇順\n",
"\n",
"Runtime 320 ms\n",
"Beats 65.78%\n",
"\n",
"```\n",
"\n",
"## 4) 要点解説\n",
"\n",
"* **高速化の勘所**\n",
"\n",
" * CTE+ウィンドウは可読性は高い一方で、**CTE のマテリアライズ**や**ウィンドウのフレーム管理**でコスト増になることがあります。\n",
" → 本問は「全体で `MAX(id)` を 1 回」分かればよいので、**定数サブクエリ JOIN**がシンプルかつ安定。\n",
" * **ビット演算**で偶奇判定(`id & 1`)にすると、`%` より演算コストが低め。\n",
" * `ORDER BY id` は必須(仕様)。`ORDER BY` は出力段で 1 回だけ走る構成にする。\n",
"\n",
"* **可読性 vs. 性能**\n",
"\n",
" * 可読性重視なら、これまでの CTE+`MAX(id) OVER()` も正解です。\n",
" * ランタイムが拮抗しているなら、**`JOIN (SELECT MAX(id))` + `& 1`** をまず試すのが無難。\n",
"\n",
"## 5) 計算量(概算)\n",
"\n",
"* いずれもフルスキャン主体で **O(N)**。\n",
"\n",
" * `JOIN (SELECT MAX(id))` の内側は 1 行だけの派生表で評価回数は 1 回。\n",
" * `UNION ALL` 案は 3 本のスキャンだが述語が軽く、条件分岐が少ないため実測で伸びる場合あり。\n",
"\n",
"## 6) 図解(Mermaid 超保守版)\n",
"\n",
"```mermaid\n",
"flowchart TD\n",
" A[Seat 入力] --> B[\"定数サブクエリで MAX(id)\"]\n",
" B --> C[\"CASE と &1 で ±1 変換\"]\n",
" C --> D[id 昇順で整列]\n",
" D --> E[出力 id, student]\n",
"```\n",
"\n",
"---\n",
"\n",
"### 小さな改善チェックリスト\n",
"\n",
"* [ ] `CASE` の偶奇判定を `id & 1` に置換\n",
"* [ ] `MAX(id)` は **CROSS/JOIN で 1 回だけ取得**\n",
"* [ ] 実行計画を確認(`EXPLAIN`)し、CTE がマテリアライズされていないかを見る\n",
"* [ ] データ量が大きい場合は `ORDER BY id` のためのソートコストが支配的。`innodb_buffer_pool_size` が十分か確認(競技環境では不可だが実務では重要)\n",
"\n",
"このあたりを適用して、あなたの 316ms / 321ms をもう一段押し下げられる可能性があります。\n",
"\n"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading