10.3. 明示的な JOIN 句でプランナを制御する

PostgreSQL 7.1 から明示的な JOIN 構文を使って問い合わせプランナをある程度制御できるようになりました。どうしてこういうことが問題になるのか、まずその背景を見る必要があります。

単純な問い合わせ、たとえば

SELECT * FROM a, b, c WHERE a.id = b.id AND b.ref = c.id;

では、プランナは自由に与えられたテーブルをいろんな順で結合することができます。 たとえば、WHERE 条件の a.id = b.id を使ってまず A と B を結合し、他の WHERE 条件を使ってその結果に C を結合するといった計画を立てることができます。あるいは、B と C を結合し、その結果に A を結合することもできます。 あるいは、A と C を結合し、その結果に B を結合することもできるでしょう。しかし、それでは効率がよくありません。なぜなら、結合の最適化を行うために適用できる条件が WHERE 句にないので、A と C の全直積が作られるからです。(PostgreSQL のエキュゼキュータでは、結合はすべて 2 つのテーブルの間で行われるため、このようにしてひとつひとつ結果を作っていかなければなりません。)重要なのは、これらの違った結合の方法は意味的には結果として同じなのですが、実行コストは大きく違うということです。ですから、プランナはもっとも効率の良いプランを探すために可能なプランをすべて検査します。

結合の対象がせいぜい 2、3 個のテーブルなら心配するほど結合の種類は多くありません。しかし、テーブルの数が増えると可能な結合の数は指数関数的に増えていきます。10 程度以上にテーブルが増えると、すべての可能性をしらみつぶしに探索することはもはや実用的ではなくなります。 6 や 7 個のテーブルでさえも、計画を作成する時間が無視できなくなります。 テーブルの数が多すぎるときは、PostgreSQL のプランナはしらみつぶしの探索から、限られた可能性だけを探索する遺伝的確率的な探索へと切り替わります (切り替えの閾値はPostgreSQL 7.3 管理者用ガイドに記述されている GEQO_THRESHOLD 実行時パラメータで設定されます)。遺伝的探索は短い時間で探索を行いますが、必ずしも最適なプランを見つけるとは限りません。

外部結合が含まれるような問い合わせでは、プランナには通常の(内部)結合よりもずっと選択の余地が小さくなります。たとえば、次のような問い合わせを考えます。

SELECT * FROM a LEFT JOIN (b JOIN c ON (b.ref = c.id)) ON (a.id = b.id);

この問い合わせの検索条件は前述の例と表面的には似ているように思えますが、B と C の結合結果の行に適合しない A の各行が出力されなければならないため、意味的には異なります。 したがって、ここではプランナには結合順に関して選択の余地がありません。まず B と C を結合し、その結果に A を結合しなければならないのです。そういうわけで、この問い合わせでは計画を立てるのに要する時間は前の例よりも短くなります。

論理的には内部結合に関して制約を設ける必要がないにも関わらず、PostgreSQL の問い合わせプランナは、すべての明示的な JOIN 構文で結合順が制約されるものと見なします。 したがって、以下のすべての問い合わせは同じ結果となりますが、2 番目と 3 番目の問い合わせは最初のものよりも短い時間で計画を立てることができます。

SELECT * FROM a, b, c WHERE a.id = b.id AND b.ref = c.id;
SELECT * FROM a CROSS JOIN b CROSS JOIN c WHERE a.id = b.id AND b.ref = c.id;
SELECT * FROM a JOIN (b JOIN c ON (b.ref = c.id)) ON (a.id = b.id);

この効果はたった 3 つのテーブルでは気にするほどのものではありませんが、多くのテーブルを結合する際には最後の頼みの綱になるかもしれません。

検索時間を節約するために、結合順を完全に束縛する必要はありません。なぜなら、単純な FROM リストの JOIN 演算子を使っても構わないからです。たとえば、次の例です。

SELECT * FROM a CROSS JOIN b, c, d, e WHERE ...;

ではプランナは他のテーブルと結合する前に A と B を結合しますが、それ以外については特に拘束はありません。この例では、結合順の候補は 5 の階乗分の 1 に減ります。

外部結合と内部結合を含む複雑な問い合わせでは、外部結合の内部で実行される内部結合の良い結合順をプランが探索するのを妨害しない方が良いかもしれません。 JOIN 構文で直接そのように指定することもできますが、副問い合わせを使って構文的な制約を回避することもできます。たとえば、次の例です。

SELECT * FROM d LEFT JOIN
        (SELECT * FROM a, b, c WHERE ...) AS ss
        ON (...);

ここでは、D の結合は問い合わせプランの中では最終段階になければならないものの、A、B、C の結合順に関してはプランナは自由に考えることができます。

このようにしてプランナの探索を制約するのは、計画を立てる時間を節約するのみならず、プランナが良い問い合わせ計画を立てるように仕向けるためにも役立つテクニックです。 もしそのままではプランナが不適切な結合順を選択するようなら、JOIN 構文を使ってプランナがより適切な結合順を選ぶように強制することができます。 これには、より適切な結合順が分かっていることが前提となります。実験してみることをお勧めします。