ページの作成
親となるページを選択してください。
親ページに紐づくページを子ページといいます。
例: 親=スポーツ, 子1=サッカー, 子2=野球
子ページを親ページとして更に子ページを作成することも可能です。
例: 親=サッカー, 子=サッカーのルール
親ページはいつでも変更することが可能なのでとりあえず作ってみましょう!
MySQL 8.0未満には再帰CTE(WITH RECURSIVE)が存在しないため、親子関係で表される階層構造(ツリー)をSQLだけで扱うには「隣接リスト+自己結合やアプリ側ループ」「経路列挙(パス文字列)」「入れ子集合モデル(Nested Set)」のいずれかで設計する必要があります。本記事では、それぞれの仕組みとSQL例、長所短所、そして8.0以降への移行指針までを整理します。
| この記事の要点 |
|---|
|
背景:なぜ8.0未満は「特別な手法」が必要なのか
カテゴリツリー、組織図、コメントのスレッド、部品表(BOM)など、現実のデータには「親をたどると先祖が連なり、子をたどると子孫が広がる」階層構造が頻繁に登場します。こうした構造を扱う最も標準的な方法は、ノードに自分の親を指す列(例:parent_id)を持たせる隣接リスト(adjacency list)です。直感的で更新も簡単ですが、「あるノードの子孫を全部取りたい」「ルートからの深さを知りたい」といった階層をまたぐ問い合わせを1クエリで書くには再帰処理が要ります。
再帰的な問い合わせを標準SQLで表現する仕組みが再帰共通テーブル式(再帰CTE、WITH RECURSIVE)です。MySQLで再帰CTEが利用できるようになったのはMySQL 8.0.1からで、それ以前の系列(5.5/5.6/5.7など、いわゆる8.0未満)では使えません。そのため8.0未満の環境では、再帰CTEに頼らずに階層を扱う「別の設計」または「別の問い合わせ技法」が必要になります。本記事の主題はまさにこの点です。
手法①:隣接リスト+自己結合 / アプリ側ループ
各行が自分の親IDだけを持つ、最も基本的なモデルです。テーブル定義は次のようになります。
|
CREATE TABLE category ( |
「直接の親子」を結ぶだけなら自己結合で十分です。たとえば各カテゴリと、その親カテゴリ名を並べるには次のように書きます。
|
SELECT c.id, c.name, p.name AS parent_name |
問題は「任意の深さの子孫すべて」を取得したい場合です。再帰CTEが無い8.0未満では、主に次の3通りで対応します。
- 段数を固定した自己結合:階層の最大深さがあらかじめ分かっている(例:最大4階層)場合、その回数だけ
LEFT JOINを重ねる。深さが増えるとSQLが破綻するため、固定階層向け。 - アプリ側ループ:まずルートを取得し、得られたIDを
WHERE parent_id IN (...)に渡して1段ずつSQLを発行し、結果が空になるまでアプリケーション側で繰り返す。実装が分かりやすく、深さ無制限に対応できる。 - ユーザー変数を使った擬似再帰:セッション変数(例:
@val)に取得済みIDを連結しながら、FIND_IN_SETで子を辿る技法。1クエリで子孫を集められるが、変数の評価順序に依存するため可読性・移植性は低い。
段数固定の自己結合の例(最大3階層分の子孫を平坦に取得)は次のとおりです。
|
SELECT l0.id AS root_id, |
手法②:経路列挙(パスを文字列で持つ)
経路列挙(path enumeration、materialized path)では、各ノードがルートからそのノードまでの経路を文字列として保持します。たとえば /1/4/9/ のように、祖先のIDを区切り文字で連結した path 列を追加します。
|
CREATE TABLE category_pe ( |
この設計の利点は、子孫の一括取得が LIKE 一発で書ける点です。あるノードのパスが /1/4/ なら、その配下すべては「パスが /1/4/ で始まる行」になります。
|
-- ノード id=4 の子孫をすべて取得 |
祖先の取得も、パスを区切り文字で分解すれば求められます。深さは path 内の区切り文字の数から計算でき、ツリー順の表示も ORDER BY path で自然に得られます。前方一致 LIKE 'prefix%' はインデックスを活用できるため、読み取りは比較的高速です。
手法③:入れ子集合モデル(Nested Set, lft/rgt)
入れ子集合モデル(nested set model)は、ツリーを「左から右へ深さ優先で巡回したときの訪問順」として数値化する手法です。各ノードに左端を表す lft と右端を表す rgt の2つの値を持たせ、子孫はすべて親の lft と rgt の間に入れ子状に収まるという性質を使います。
|
CREATE TABLE category_ns ( |
子孫の取得は範囲条件だけで書け、再帰もアプリ側ループも不要です。
|
-- 親ノード(parent.id = 1)の子孫をすべて取得 |
逆に「あるノードの祖先」も、自分の lft を挟み込む(lft < 自分の lft かつ rgt > 自分の rgt)ノードを探すだけで求められます。読み取り性能は3手法の中でも優秀です。一方で、ノードを1つ挿入・削除・移動すると、その影響範囲にある多数の行の lft/rgt を一斉に振り直す必要があり、更新コストが大きいのが弱点です。
各手法の比較
| 手法 | 保持する情報 | 子孫取得 | 更新コスト | 実装の複雑さ |
|---|---|---|---|---|
| 隣接リスト | parent_id | 再帰CTEが無いと弱い(自己結合は段数固定/アプリ側ループ) | 小(1行更新) | 低い |
| 経路列挙 | path文字列 | 容易(前方一致LIKE) | 中(部分木移動でパス書き換え) | 中 |
| 入れ子集合 | lft / rgt | 容易かつ高速(範囲条件) | 大(挿入・移動で多数行を再採番) | 高い |
おおまかな指針として、更新が頻繁で読み取りが単純なら隣接リスト、子孫の一括読み取りが多く更新がそこそこなら経路列挙、読み取りが圧倒的に多く更新がまれなら入れ子集合が向く傾向があります。要件次第のため、実データの規模とアクセスパターンで判断してください。
MySQL 8.0以降なら再帰CTEが使える
環境を8.0以降へ更新できるなら、隣接リストのまま再帰CTEで階層を辿るのが最も素直です。データモデルを変えずに済み、可読性も高くなります。
|
-- MySQL 8.0以降: 隣接リストを再帰CTEで展開 |
移行の指針としては、新規設計なら8.0以降を前提に隣接リスト+再帰CTEを基本とし、8.0未満を使い続ける必要がある間だけ、本記事の3手法から要件に合うものを選ぶ、という考え方が分かりやすいでしょう。なお、入れ子集合や経路列挙は8.0以降でも有効な設計であり、再帰CTEの登場によって即座に不要になるわけではありません。
落とし穴
| つまずきやすいポイント |
|---|
|
よくある質問(FAQ)
Q. MySQLで再帰CTE(WITH RECURSIVE)はいつから使えますか?
A. MySQL 8.0.1で導入されました。したがって8.0未満(5.7以前)では利用できず、本記事のいずれかの手法が必要になります。
Q. 結局どの手法を選べばよいですか?
A. 一概には決まりません。更新が多く読み取りが単純なら隣接リスト、子孫の一括取得が多いなら経路列挙、読み取り中心で更新がまれなら入れ子集合が候補です。実データの規模・更新頻度・問い合わせパターンを見て選んでください。
Q. 8.0未満で隣接リストの子孫を1クエリで取りたい場合は?
A. 階層数が固定なら段数分の自己結合で対応できます。深さが不定なら、アプリ側で1段ずつ繰り返し問い合わせるか、セッション変数と FIND_IN_SET を使う擬似再帰の技法を用います。後者は可読性・移植性が低い点に留意してください。可能であれば8.0以降へ更新し、再帰CTEを使うのが最も簡潔です。
ページの作成
親となるページを選択してください。
親ページに紐づくページを子ページといいます。
例: 親=スポーツ, 子1=サッカー, 子2=野球
子ページを親ページとして更に子ページを作成することも可能です。
例: 親=サッカー, 子=サッカーのルール
親ページはいつでも変更することが可能なのでとりあえず作ってみましょう!
子ページはありません
- ダウンロード&インストール方法(Windows)
- インストール方法(Linux)
- コマンド一覧
- SQL
- データ型
- 関数
- 管理ツール
- 設定
- パフォーマンスチューニング関連
- エクスポートおよびインポート
- エラー&トラブル
- 文字コードの確認
- 実行中の SQL の状態確認およびプロセスキルの方法
- パスワードの無効化設定
- root ユーザーの初期パスワード確認方法
- rootユーザーのパスワード変更方法
- LIMIT, OFFSET の始まりと挙動
- mysqlのバージョン確認方法
- MySQLで実行計画を表示する方法
- レプリケーションのステータス確認方法
- 中央値の導き方(バージョン8未満)
- 階層SQL(バージョン8未満)
- パーセンタイルの導き方
- 特定スキーマの全テーブルの全カラム情報を取得する方法
人気ページ
- 1 Eclipseで「サーバーに追加または除去できるリソースがありません。」の原因と対処法
- 2 tomcat の起動 / 停止ログと catalina.log・catalina.out の違い
- 3 JavaScript base URL 取得方法|window.location.origin と SSR/Node.js 対応
- 4 YouTube Data API v3 エラー一覧|403/400/404 の主要原因と切り分け
- 5 Spring Frameworkのアノテーション一覧
- 6 Laravel エラー一覧|500/Blade/DB 接続/ルーティングの代表エラー
- 7 3Dグラフィックスとは|モデリング/レンダリング/主要ソフトウェア (Blender / Maya)
- 8 【Spring】@Valueアノテーションとは
- 9 CATALINA_HOME の確認方法 (Linux / Mac)
- 10 【Spring】@Autowiredアノテーションとは
最近更新/作成されたページ
- IPv6とは|128bitアドレス・コロン16進表記/::省略・リンクローカル・SLAAC・デュアルスタック NEW 2026-06-22 12:34:44
- MAC アドレスフィルタリングの仕組みと限界 | ネットワーク入門 NEW 2026-06-22 12:19:10
- VPNとは|暗号トンネル・サイト間/リモートアクセス・IPsec/SSL-VPN/WireGuardを解説 NEW 2026-06-22 12:19:10
- WebSocket とは 全二重リアルタイム通信 ws/wss | ネットワーク入門 NEW 2026-06-22 12:17:25
- HTTP/2 とは 多重化・HPACK・バイナリフレーム | ネットワーク入門 NEW 2026-06-22 12:17:25
- Web通信プロトコル入門 HTTP/2・HTTP/3・WebSocket・gRPC・WebRTC | ネットワーク入門 NEW 2026-06-22 12:17:25
- gRPC とは HTTP/2 + Protocol Buffers の高速 RPC | ネットワーク入門 NEW 2026-06-22 12:17:25
- HTTP/3 (QUIC) とは UDP ベースの低遅延 Web 通信 | ネットワーク入門 NEW 2026-06-22 12:17:25
- WebRTC とは ブラウザ間 P2P の音声・映像・データ通信 | ネットワーク入門 NEW 2026-06-22 12:17:25
- 証明書と認証局(CA)とは|X.509・信頼チェーン・DV/OV/EV・失効(CRL/OCSP)を解説 NEW 2026-06-22 12:17:24
- ファイアウォールとは|パケットフィルタ・ステートフル・DMZ・次世代FW(L4/L7)を解説 NEW 2026-06-22 12:17:24
- iptables/nftablesとは|テーブル・チェーン・ルール例・永続化をLinux視点で解説 NEW 2026-06-22 12:17:24
- HAProxy とは frontend/backend と設定例 | ネットワーク入門 NEW 2026-06-22 12:17:24
- TLS/SSLの仕組み|ハンドシェイク・暗号スイート・前方秘匿性・証明書検証をわかりやすく解説 NEW 2026-06-22 12:17:24
- CDN とは エッジキャッシュ・TTL・Cloudflare/CloudFront | ネットワーク入門 NEW 2026-06-22 12:17:24
コメントを削除してもよろしいでしょうか?