無限ループを避ける
深さの制限とサイクル検出を行います。
「無限ループを避ける」はCoddyKit上の無料SQL Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはSQL Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 SQL Academyコースには全4レッスンが含まれています。
無限ループの問題
再帰CTEは強力ですが、重大なリスクもあります。クエリが基底ケースに到達しなければ、永遠にループして利用可能なメモリを使い果たし、データベースセッションがクラッシュします。
無限ループが発生する理由を理解することが、防止に向けた第一歩です。
ループが終わらないのはなぜか
再帰CTEは、再帰項が新しい行を生成し続け、新しい行が生成されない状態に一度も到達しない場合、無期限にループします。
これは通常、終了条件がない、または正しくない場合と、ノードAがBを指し、BがAを指し返すような循環データがある場合の2つの状況で発生します。
-- Simple recursive CTE that WOULD loop forever
-- (do NOT run this as-is; illustration only)
WITH RECURSIVE counter AS (
SELECT 1 AS n -- base case
UNION ALL
SELECT n + 1 -- recursive term
FROM counter
-- no WHERE clause to stop it!
)
SELECT n FROM counter;深さ制限を追加する
最も簡単な対策は深さカウンターです。再帰ステップごとに1ずつ増える列を追加し、最大深度を超えた時点で停止します。
これにより、データの内容にかかわらず処理の終了が保証され、設定した上限が安全弁になります。
WITH RECURSIVE counter AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1
FROM counter
WHERE n < 10 -- stop at depth 10
)
SELECT n FROM counter;階層クエリで深さを制限する
従業員階層をたどるときは、パスと一緒に深さを追跡できます。WHERE depth < 5句により、データにより深い階層や循環リンクがあっても、5階層を超えた探索を防止できます。
CREATE TEMP TABLE employees (
id INT PRIMARY KEY,
name TEXT,
manager_id INT
);
INSERT INTO employees VALUES
(1, 'Alice', NULL),
(2, 'Bob', 1),
(3, 'Carol', 2),
(4, 'Dave', 3);
WITH RECURSIVE hierarchy AS (
SELECT id, name, manager_id, 1 AS depth
FROM employees
WHERE manager_id IS NULL -- root
UNION ALL
SELECT e.id, e.name, e.manager_id, h.depth + 1
FROM employees e
JOIN hierarchy h ON e.manager_id = h.id
WHERE h.depth < 5 -- depth limit
)
SELECT id, name, depth FROM hierarchy ORDER BY depth, id;循環検出とは
グラフデータで循環が発生するのは、エッジをたどった結果、すでに訪問したノードに戻ってくる場合です。たとえば、A → B → C → Aのような状態です。
深さ制限を使えば循環データでもクエリは終了しますが、循環がどこにあるかまではわかりません。明示的な循環検出なら、それを特定できます。
CREATE TEMP TABLE edges (
from_node INT,
to_node INT
);
-- Introduce a cycle: 1->2->3->1
INSERT INTO edges VALUES
(1, 2),
(2, 3),
(3, 1), -- cycle back to 1
(1, 4); -- also a non-cyclic branch
SELECT * FROM edges;配列で訪問済みノードを追跡する
堅牢な循環検出手法の1つは、訪問済みノードIDの配列を再帰処理に引き継ぐことです。次のノードを訪問する前に、すでに配列に含まれているかを確認します。含まれていれば、そのノードをスキップします。
PostgreSQLでは、ANY(array)演算子と||配列追加演算子によって簡単に実装できます。
WITH RECURSIVE traverse AS (
-- Start from node 1
SELECT from_node,
to_node,
ARRAY[from_node] AS visited
FROM edges
WHERE from_node = 1
UNION ALL
SELECT e.from_node,
e.to_node,
t.visited || e.from_node
FROM edges e
JOIN traverse t ON e.from_node = t.to_node
WHERE NOT (e.from_node = ANY(t.visited)) -- skip visited nodes
)
SELECT from_node, to_node, visited
FROM traverse;CYCLE句(PostgreSQL 14以降)
PostgreSQL 14では、再帰CTE用の組み込みCYCLE句が導入されました。循環が検出されたときにtrueになるブール型フラグと、たどったパスを記録する配列の2つの列が自動的に追加されます。
配列を手動で管理するよりもすっきり記述できます。
WITH RECURSIVE traverse AS (
SELECT from_node, to_node
FROM edges
WHERE from_node = 1
UNION ALL
SELECT e.from_node, e.to_node
FROM edges e
JOIN traverse t ON e.from_node = t.to_node
)
CYCLE from_node SET is_cycle USING path
SELECT from_node, to_node, is_cycle, path
FROM traverse;深さ制限と循環検出を組み合わせる
深さ制限と循環検出を併用すると、最も強力な安全策になります。
- 深さ制限は、データの品質にかかわらず機能する絶対的な上限になります。
- 循環検出は、ループが見つかった時点ですぐに停止するため、不要な反復を節約できます。
本番環境のクエリでは、これらの安全策のうち少なくとも1つを必ず適用してください。
WITH RECURSIVE traverse AS (
SELECT from_node,
to_node,
1 AS depth,
ARRAY[from_node] AS visited
FROM edges
WHERE from_node = 1
UNION ALL
SELECT e.from_node,
e.to_node,
t.depth + 1,
t.visited || e.from_node
FROM edges e
JOIN traverse t ON e.from_node = t.to_node
WHERE t.depth < 10 -- depth limit
AND NOT (e.from_node = ANY(t.visited)) -- cycle guard
)
SELECT from_node, to_node, depth, visited
FROM traverse;完全なパスを文字列として作成する
循環検出と併せて、人が読みやすい文字列として完全な探索パスを記録すると便利です。ノードIDを -> で区切って連結すると、グラフ内でたどった経路を簡単に表示したりデバッグしたりできます。
WITH RECURSIVE traverse AS (
SELECT from_node,
to_node,
1 AS depth,
ARRAY[from_node] AS visited,
from_node::TEXT AS path_str
FROM edges
WHERE from_node = 1
UNION ALL
SELECT e.from_node,
e.to_node,
t.depth + 1,
t.visited || e.from_node,
t.path_str || ' -> ' || e.from_node::TEXT
FROM edges e
JOIN traverse t ON e.from_node = t.to_node
WHERE t.depth < 10
AND NOT (e.from_node = ANY(t.visited))
)
SELECT from_node, to_node, path_str, depth
FROM traverse
ORDER BY depth;max_recursive_iterationsを設定する
一部のデータベース(MariaDB、古いMySQL)では、セッション変数を使って再帰回数に上限を設定します。PostgreSQLでは、自分で記述した深さカウンターを使用するか、ステートメントレベルのタイムアウトを利用するのが同等の方法です。
statement_timeoutを設定すると、設定時間を超えた暴走クエリを強制終了する最終手段の安全策になります。
-- PostgreSQL: set a statement timeout as a safety net
SET statement_timeout = '5s';
-- Now any query that runs longer than 5 seconds is cancelled
WITH RECURSIVE counter AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM counter WHERE n < 1000000
)
SELECT MAX(n) FROM counter;
-- Reset to default when done
SET statement_timeout = '0';適切な深さ制限を選ぶ
すべてのデータに適用できる深さ制限はありません。データで現実的に想定される最大深度に基づいて設定してください。
- 組織図が10~15階層を超えることはめったにありません。余裕を持たせて
depth < 20を使用します。 - ファイルシステムのツリーは、50~100階層の深さになることがあります。
- ソーシャルネットワークのグラフ探索では、通常3~6ホップに制限します。
有効なデータを十分に取得できる高さにしつつ、暴走クエリを早期に検出できる低さに設定してください。
-- Example: org chart with a generous but safe depth cap
WITH RECURSIVE org AS (
SELECT id, name, manager_id, 1 AS depth
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, e.manager_id, o.depth + 1
FROM employees e
JOIN org o ON e.manager_id = o.id
WHERE o.depth < 20 -- realistic upper bound for an org chart
)
SELECT id, name, depth
FROM org
ORDER BY depth, name;深さ制限と循環検出の比較
どちらの手法を使うべきでしょうか。
まとめ:再帰クエリを安全に保つ
ここでは、再帰CTEで無限ループを避ける方法をまとめます。
- 深さ制限 — カウンター列を追加し、
WHERE depth < Nで停止します。常に有効で、実装も簡単です。 - 配列ベースの循環検出 — 訪問済みノードIDを配列で引き継ぎ、すでに含まれているノードをスキップします。最初の循環で早期に停止します。
- CYCLE句(PostgreSQL 14以降) —
is_cycle列とpath列を使った循環追跡を自動化する組み込み構文です。 - statement_timeout — 暴走クエリに対するデータベースレベルの安全策であり、適切なロジックの代わりにはなりません。
- 本番環境では、最も確実な安全策として深さ制限と循環検出を組み合わせてください。
これらの手法を使えば、データベースをクラッシュさせるリスクを抑えながら、階層やグラフを安心してたどれるようになります。
よくある質問
「無限ループを避ける」レッスンは無料ですか?
はい。「無限ループを避ける」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、SQL Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 SQL Academyコースには全4レッスンが含まれています。
「無限ループを避ける」で何を学びますか?
深さの制限とサイクル検出を行います。 ブラウザで直接実行するハンズオンコードでSQL Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
SQL Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのSQL Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。
「無限ループを避ける」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このSQL Academyレッスンでコードを書いて実行できますか?
はい。すべてのSQL Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- 再帰CTEの仕組み
- カテゴリーツリーをたどる
- 系列とシーケンスを生成する
- 無限ループを避ける