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

明示的な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つのテーブルの間で行われるため、このようにして1つひとつ結果を作っていかなければなりません)。 重要なのは、これらの違った結合の方法は意味的には同じ結果なのですが、実行コストは大きく違うということです。 ですから、プランナは最も効率の良いプランを探すために可能なプランを全て検査します。

結合の対象がせいぜい2、3個のテーブルなら心配するほど結合の種類は多くありません。 しかし、テーブル数が増えると可能な結合の数は指数関数的に増えていきます。 10程度以上にテーブルが増えると、全ての可能性をしらみつぶしに探索することはもはや実用的ではなくなります。 6や7個のテーブルでさえも、計画を作成する時間が無視できなくなります。 テーブルの数が多過ぎる時は、PostgreSQLのプランナはしらみつぶしの探索から、限られた可能性だけを探索する遺伝的確率的な探索へと切り替わります (切り替えの閾値は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を結合しなければならないのです。 そういうわけで、この問い合わせでは計画を立てるのに要する時間は前の例よりも短くなります。 その他の場合、プランナが安全な結合順を複数決定できる可能性があります。 たとえば、以下を考えてみます。

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

この場合、Aを先にBと結合してもCと結合しても有効です。 現時点では、FULL JOINのみが完全に結合順を制限します。 LEFT JOINRIGHT JOINを含む、ほとんどの実環境では、何らかの拡張に再調整することができます。

明示的な内部結合構文(INNER JOINCROSS JOIN、装飾のないJOIN)は、意味的にはFROM内の入力リレーションの列挙と同じです。 したがって、結合順を制約する必要はありません。

ほとんどの種類のJOINは完全に結合順を制約しませんが、 PostgreSQL問い合わせプランナに、すべてのJOIN句に対してとりあえず結合順を制限させることができます。 例えば、以下の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);

しかし、プランナにJOINの順番を守るように伝えた場合、2番目と3番目の問い合わせは最初のものよりも短い時間で計画を立てることができます。 この効果はたった3つのテーブルでは気にするほどのものではありませんが、多くのテーブルを結合する際には最後の頼みの綱になるかもしれません。

プランナを強制的に明示的なJOINに潜在する結合順に従わせるには、join_collapse_limit実行時パラメータを1に設定してください (以下で他の取り得る値について説明します)。

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

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

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

こうした方法でプランナの検索に制約を加えることは、計画作成時間の短縮とプランナに対する優れた問い合わせ計画への方向付けの両方のために有用な技法です。 プランナが劣った結合順をデフォルトで選択するのであれば、JOIN構文経由でより良い順番を選択するように強制することができます。 ただし、より良い順番を理解しているという前提があります。 これには実験することを勧めます。

問い合わせ作成時間に影響する密接に関連した問題として、副問い合わせをその親問い合わせに折り畳むことがあります。 例えば、以下を考えてみます。

SELECT *
FROM x, y,
    (SELECT * FROM a, b, c WHERE something) AS ss
WHERE somethingelse;

こうした状況は、結合を含むビューを使用する際に現れます。 そのビューのSELECTルールはビューを参照するところに挿入され、上のような問い合わせを生成します。 通常、プランナは副問い合わせを親問い合わせに折り畳み、以下を生成します。

SELECT * FROM x, y, a, b, c WHERE something AND somethingelse;

これは通常、副問い合わせの計画を別途作成するより優れた計画を作成します (例えば、外部のWHERE条件はXをAに結合するようになり、まずAの多くの行が取り除かれます。 これにより、副問い合わせの完全な論理的出力が不要になります)。 しかし、同時に計画作成時間が増加します。 この場合、2つの3通りの結合問題から5通りの結合問題になります。 候補数は指数関数的に増加するため、これは大きな違いになります。 プランナは大規模な結合検索問題で息詰まらないように、もしfrom_collapse_limit個のFROM項目が親問い合わせで発生してしまう場合は副問い合わせの折り畳みを抑制します。 この実行時パラメータの値を上下に調整することで計画作成時間と計画の質をトレードオフすることができます。

両者はほとんど同じことを行うため、from_collapse_limitjoin_collapse_limitは似たような名前になっています。 片方は副問い合わせの"平坦化"をプランナがいつ行うかを制御し、もう片方は明示的な結合の平坦化をいつ行うかを制御します。 通常、join_collapse_limitfrom_collapse_limitと同じ値に設定する(明示的な内部結合と副問い合わせの動作を同じにする)か、join_collapse_limitを1に設定する(明示的な結合で結合順を制御したい場合)かのどちらかを行います。 しかし、計画作成時間と実行時間の間のトレードオフを十分に行う場合は、これらを別の値に設定しても構いません。