メインコンテンツへスキップ

VillageSQL is a drop-in replacement for MySQL with extensions.

All examples on this page work on VillageSQL. Install Now →
CTE と派生テーブルは同じ行を返すため、どちらを選ぶかはたいてい読みやすさの 問題です。読みやすさだけの問題でなくなるのは、オプティマイザがいずれかを実体化する ときで、これは多くの解説が示唆するよりも頻繁に起こります。このガイドは、どちらに なったかを見分けることと、CTE が誤った形になるケースについて説明します。 構文そのものはチュートリアルが教えています。 共通テーブル式、再帰 CTE、 および派生テーブルです。 このページのすべてのプランは、Sakila サンプルデータベースに対する 1 台のサーバーから 得たものです。コストと行数の見積もりは、InnoDB のサンプリングされた統計と optimizer_switch の設定に由来するため、読者の数値は異なります。そのまま当てはまるのは 各プランの形です。

ルール: マージ可能かどうか

CTE は外側のクエリに折りたたまれるか、先に一時テーブルとして構築されるかの どちらかであり、どちらになるかは、CTE を何回参照したかではなく、CTE の中身に よって決まります。 本体が WHERE と JOIN を伴う単純な SELECT である CTE はマージされ、 プランから完全に消えます。
CTE ノードはなく、外側の条件はインデックスに届いています。GROUP BY、 DISTINCT、LIMIT、UNION、またはウィンドウ関数を含む CTE はマージできません。 外側のクエリの条件をそれらの操作の前に適用すると、等価ではなくなるためです。 そのような CTE は実体化されます。 以下のプランは、その 2 番目のケースを示しています。WITH という語ではなく、 CTE の中身に関するものとして読んでください。

どちらになったかを確認する

EXPLAIN FORMAT=TREE は、そのままの言葉でそれを示します。デフォルトの EXPLAIN は 示しません。ここにあるクエリでは、その Extra 列にその語は現れません。
CTE は 1 回しか参照されていないのに、Materialize CTE counts です。その中の GROUP BY がこれを強制しており、サーバーは行を一時テーブルに書き込んでから 読み戻します。 同じ GROUP BY を含む派生テーブルは、同じプランになります。 FROM (SELECT rating, COUNT(*) ... GROUP BY rating) AS counts と書くと、ツリーは ノードの名前を除いて同一であり、コストも同じです。したがって、参照が 1 つの クエリでは、CTE が派生テーブルに優る点は何もありません。2 つの形は読み方が 異なるだけで、オプティマイザは両者を同じように扱います。 同じ CTE を 2 回参照すると、表現が if needed に変わり、2 番目の参照は同じ 実体化された結果を再利用します。
ここが、2 つの形が本当に分かれる場所です。高コストな内側のクエリは 1 回だけ 実行され、両方の参照がその結果を読みます。派生テーブルとして書くと、同じクエリは サブクエリを 2 回記述し、2 回計算します。

実体化のコスト

実体化された CTE は一時テーブルであり、これは知っておくべき 2 つのことを意味します。 ベーステーブルのインデックスを手放します。行が一時テーブルに置かれると、外側の クエリはそのテーブルをスキャンします。サーバーは、上の結合で行ったように、その場で auto_key を構築することがありますが、それは実際のテーブルにあったインデックスでは ありません。外側の条件が PRIMARY に届いた、このページ冒頭のマージされたプランと 比較してください。 プッシュダウンがこれを和らげます。derived_condition_pushdown はデフォルトで 有効であり、可能なものを内側に移動します。上の Filter: (count(0) > 200) が Materialize ノードの外側ではなく内側に現れるのはそのためです。すべてを移動 できるわけではないため、プランが、結果が保持する数よりはるかに多くの行を CTE が 生成していることを示す場合は、制限を自分で CTE の中に書いてください。

CTE が誤った形である場合

  • 1 つの短いステップ。 派生テーブルは、それが使われる場所に置いておけます。 1 行のサブクエリに名前を付けると、読者にとって一段の遠回りが増えます。
  • オプティマイザがプッシュダウンしなかったフィルタ。 プランが、CTE がすべての 行を実体化し、外側のクエリがそのほとんどを捨てていることを示す場合は、フィルタを 内側に書きます。
  • インデックスに対する行ごとの処理。 相関サブクエリまたは結合は、検索のたびに インデックスを使えます。実体化された CTE はすでにそれを手放しています。
CTE が正しい形であるのは、クエリに 2 つ以上の名前付きの段階がある場合、同じ結果が 2 回必要な場合、または代替がどこで終わるのか誰にも分からないほど深くネストした サブクエリである場合です。 また、2 つのうちデバッグしやすいのも CTE です。CTE の定義は、そこから SELECT することで 単独で実行できるため、2 番目の段階を見る前に最初の段階が正しいかどうかが分かります。 FROM 句の途中から派生テーブルを取り出して手で実行するのは、毎回手作業で行う 同じ作業です。

実際の階層に対する再帰 CTE

Sakila には階層がないため、このセクションでは独自のテーブルを使います。サンプル データの横に作成し、使い終わったら DROP TABLE employees; を実行してください。
シードがルートを選択し、再帰メンバーがテーブルをすでに生成された行に結合します。 depth 列は答えであると同時にガードでもあります。
循環が故障モードです。 A の上司が B で、B の上司が A である場合、クエリの中の 何もそれを止めず、深さの上限に達するまで実行されます。
上のクエリの WHERE oc.depth < 100 がそれを防ぎます。無限の探索を有限の探索に 変え、データがきれいかどうかに関係なく機能します。 エラーメッセージは cte_max_recursion_depth を上げるよう提案しますが、それが 正しいのは、再帰が本当に終了し、デフォルトの 1,000 回より多くの回数が必要な場合 だけです。正当な 1,500 ステップの系列は同じエラーに当たり、その場合は SET SESSION cte_max_recursion_depth = 5000 が正しい答えです。循環のあるデータでは それは誤った答えです。クエリはやはり終わらず、代わりに後で失敗します。

利用可能性と用語

CTE は MySQL 8.0 で登場し、8.4 と 9.x を含むそれ以降のすべてのリリースに存在します。 WITH 句に ERROR 1064 を返すサーバーは、それ以前のものです。SELECT VERSION() で 確認してください。 標準では、再帰 CTE の最初の部分をアンカーメンバー、2 番目の部分を再帰 メンバーと呼びます。チュートリアルは最初の部分をシードと呼びますが、これは同じ ものであり、MySQL 自身のメッセージは標準の用語を使います。

CTE は INSERT、UPDATE、および DELETE で機能する

CTE が先に評価されるため、それが特定する行は、ステートメントが何かを変更する 前に決定されます。

トラブルシューティング

関連項目