Chapter 15. SQLの拡張: 演算子

Postgresでは左単項演算子、右単項演算子、 二項演算子をサポートしています。演算子はオーバーロード可能です。つ まり、同じ演算子名を引数の数と型が異なる演算子に対して使用する事が できるということです。もし使用する演算子に曖昧な状態があり、システ ムが使用するべき正しい演算子を決定することができない場合は、エラー を返します。その場合、どの演算子を使いたいのかを明示的に指定するた めに、左/右演算項目を型キャストを行う必要があるかもしれません。

全ての演算子は、実際の処理を行なう関数を呼び出す "文法的代理"です。したがって、演算子を作成する前に、まずはその 基礎となる関数を作成する必要があります。しかし、演算子は単なる 文法的代理だけではありません。その演算子を使う問い合わせを 最適化するクエリプランナを補助するための、追加的な情報を伝える 機能を持っています。この章の大部分は、この追加的な情報について説明します。

ここで2つの複素数を加算する演算子を作成するという例を示します。 既に複素数型を定義しているものとします。まず加算を行なう関数を 作成する必要があります。その後に演算子を定義できます。

CREATE FUNCTION complex_add(complex, complex)
    RETURNS complex
    AS '$PWD/obj/complex.so'
    LANGUAGE 'c';

CREATE OPERATOR + (
    leftarg = complex,
    rightarg = complex,
    procedure = complex_add,
    commutator = +
);
   

これで下記の事を実行できるようになります。

SELECT (a + b) AS c FROM test_complex;

+----------------+
|c               |
+----------------+
|(5.2,6.05)      |
+----------------+
|(133.42,144.95) |
+----------------+
   

ここでは二項演算子をどのように作成するのかを示しました。 単項演算子を作成するには、単に、左方単項の場合はleftargを、 右方単項の場合はrightargを省略するだけです。 procedure句とargument句の2つのみがCREATE OPERATORでの必須項目です。 例で示したCOMMUTATOR句はオプションで、クエリオプティマイザへの ヒントとなります。COMMUTATOR とその他のオプティマイザへのヒントに ついての詳細は後述します。

15.1. 演算子最適化に関する情報

著者: Tom Lane氏

Postgresの演算子定義には、システムに 演算子がどうふるまうかに関する有効な事を伝える、幾つかのオプション句を 持つ事ができます。これらの句はその演算子を使用する問い合わせ実行の際に、 これらの句の情報により、かなりの速度向上がなされるので、適当な時には 常に適応できる状態にしておかなければなりません。 しかし、適応する際、それらが正しい事を確認しなければいけません。 最適化用の句を間違って使用すると、バックエンドのクラッシュ、 不思議な間違った出力、その他有害な事が起こります。 最適化用の句について解らなければ、使用しなくても構いません。 使用された時よりも問い合わせの実行が遅くなるかもしれないというだけです。

最適化用の句は、Postgresの今後のバージョンで 更に追加される可能性があります。ここで記述したものは全て、 バージョン6.5 で有効なものです。

15.1.1. COMMUTATOR句

COMMUTATOR句が与えられた場合、それはある演算子に定義された演算子 の交代演算子であると名付けます。取り得る全ての入力x 、yに対して、 (x A y)が(y B x)と等しい時、演算子Aは演算子Bの交代演算子であるといいます。 また、BはAの交代演算子となることにも注意して下さい。 例えば、特定の型用の演算子'<'と'>'は通常、互いの交代演算子になります。 演算子'+'は通常自身が交代演算子となります。しかし、演算子'-'は 通常交代演算子を持ちません。

交代された演算子の左引数の型は、その交代演算子の右引数の型と同一で、 その逆も又同様です。したがって、Postgresで 演算子を検索する時に必要なものは交代演算子の名前のみであり、 COMMUTATOR句ではそれのみが与えられていればよいです。

交代演算子である演算子を定義する場合は、単にそれを指定するだけです。 交代演算子のペアを定義する場合は少し複雑になります。 最初に他の未定義のものを参照するものをどう定義するのかということが 問題となります。この問題には下記の2つの解決方法があります。

  • 1つ目の方法は、最初の演算子を定義する際にCOMMUTATOR句を省略し、 2番目の演算子の定義では、COMMUTATOR句に最初の演算子を与えるという方法です。 Postgresは交代演算子がペアになっていることが 解っていますので、2番目の定義を見た時に、自動的に最初の定義に戻って その未定義になっているCOMMUTATOR句を設定します。

  • もう1つの方法は、両方の定義にCOMMUTATOR句を含めるというもっと素直な方法です。 Postgresは最初の定義を処理する際に、 COMMUTATORが存在しない演算子を参照している事が解ると、 システムはその演算子用の仮のエントリをシステムテーブルのpg_operatorに作成します。 この仮エントリには、Postgresがこの時点で推定できる 演算子名、左引数の型、右引数の型、及び結果の型についてのみの有効なデータが 入ります。最初の演算子のカタログエントリはこの仮エントリに結び付きます。 この後、2番目の演算子が定義されたら、システムはその仮エントリに 追加情報を更新します。更新される前に仮演算子を使用すると、 エラーメッセージが出力されます。(注意: バージョン6.5より前のPostgres ではこの方法は信頼性がありませんでしたが、今ではこの方法が推奨されています。)

15.1.2. NEGATOR句

NEGATOR句が定義された場合、それはある演算子に定義された演算子の 否定子であるとします。入力値、x とyの取り得る全ての値に対して 両方の演算子がブール値を返し、(x A y)がNOT (x B y)と等しい場合、 演算子Aは演算子Bの否定子であるといいます。また、BはAの否定子でも あることに注意して下さい。例えば、ほとんどの型では'<'と'>='は否定子の ペアとなります。演算子が自身の否定子になることは決してありません。

COMMUTATOR句とは異なり、単項演算子のペアは互いの否定子になり得ます。 それはつまり、xの取り得るすべての値に対して、(A x)が NOT (B x)に等しいこと、 また、右単項演算子の場合も同様の等式がなりたつことを意味します。

演算子の否定子は、COMMUTATOR句と同様に、その演算子の左引数、右引数の型と 同じものをとらなければなりません。演算子の名前のみはNEGATOR句で 指定されたものでなければいけません。

NOT (x = y) といった式をx <> yといった形に単純化させることが可能なので、 NEGATORを与える事は問い合わせオブティマイザにとって非常に役に立ちます。 他の再配置の結果としてNOTが挿入されることがありますので、 この現象は想像以上に多く起こります。

否定子のペアは、上記の交代演算子のペアで説明した方法と同じ方法で 定義することができます。

15.1.3. RESTRICT句

RESTRICT句が与えられた場合、それは、その演算子用の制限選択評価関数を指定します。 (演算子名ではなく関数名であることに注意して下さい。)RESTRICT句はブール値を 返す二項演算子に対してのみ有効です。制限選択評価の背景には、 指定した演算子と特定の定数に対して、下記のWHERE句の条件を満たすものは テーブルの中でどのくらいの割合の行が存在するかを推測することです。

    		field OP constant
   
この形式を持ったWHERE句を使って、どのくらいの行が除外されるのかを 通知することで、オブティマイザの手助けをします。(定数値が左項にあったら 何が起こるかという疑問が生じるかも知れませんが、それは COMMUTATORが存在する理由の1つでもあります。)

新しい制限選択評価関数の記述方法はこの章の内容を越えていますが、 幸いな事に、大抵の場合システムが持つ標準的な評価関数を使って、 多くの自作演算子用が作成できます。標準的な制限評価関数には 下記のものがあります。

	eqsel		for =
	neqsel		for <>
	scalarltsel	for < or <=
	scalargtsel	for > or >=
   
これらがカテゴリであることは少し奇妙に思えるかもしれませんが、 これらには歴とした意味があります。'='は一般的にテーブル内の行の 小さな部分を受け付けます。'<>'は一般的に小さな部分を除きます。 '<'は、指定した定数がテーブルカラム(これはVACUUM ANALYZEによって 収集される情報で、選択評価関数で使用できる ように作成されます。) のとる値の範囲のどの辺りにあるのかに依存する量の部分を受け付けます。 '<='は'<'よりも少しだけ大きな部分を受け付けます。この差は比較に 用いた定数と同じ部分のためですが、特に大雑把な推測以上のことを行なうのは 適切ではありませんので、区別する価値がないといえる位近い値です。 '>'と'>='についても同様な事がいえます。

非常に高い/低い選択性をもつ演算子に、全く等しい場合/等しくない場合で、 eqselかneqselを使用しないでおく事も可能です。 例えば、ほぼ等しい幾何演算子では テーブルのエントリの小部分にのみに合うものと仮定してeqselを使用します。

範囲比較のために数スカラーに変換された際に、敏感になる型を 比較するために、scalarltselとscalargtselを使用することも可能です。 可能であるならばsrc/backend/utils/adt/selfuncs.cにある、 convert_to_scalar()のルーチンで理解できるところに追加して下さい。 (今後、このルーチンはpg_typeテーブルの行で指定されたper-datatype関数で 置き換えられなければなりませんが、まだ行われていません。) これを行わなくても動きますが、オブティマイザの推測機能は その機能を発揮できません。

幾何演算子に対しての追加選択関数(areasel、positionsel、contsel)は src/backend/utils/adt/geo_selfuncs.cに書かれています。 このドキュメントを書いている時点では、これらは単なるスタブですが、 ともかく使いたい(あるいは改良したい)こともあるでしょう。

15.1.4. JOIN句

JOIN句が与えられた場合、それはその演算子用の結合選択評価関数の名前を示します。 (これが演算子名ではなく関数名であることに注意して下さい。) JOIN句はブール値を返す二項演算子のみ有効です。結合選択評価関数の 背景には、対象とする現在の演算子で、下記の形式のWHERE句条件をみたす行は 二つのテーブルの間でどのくらいの割合で存在するのかを推測する事があげられます。

                table1.field1 OP table2.field2
     
RESTRICT句の使用と同様、これは、いくつかの取り得る結合シーケンスのうち どれが最も仕事量が少ないように考えられるのかをオブティマイザに計算させる ことで大きなオブティマイザへの援助となります。

前と同様に、この章では結合選択評価関数の書き方を説明しませんが、 もし適用できるならば下記の標準的な評価関数のうちの一つを使う事を勧めます。

	eqjoinsel	for =
	neqjoinsel	for <>
	scalarltjoinsel	for < or <=
	scalargtjoinsel	for > or >=
	areajoinsel	for 2D area-based comparisons
	positionjoinsel	for 2D position-based comparisons
	contjoinsel	for 2D containment-based comparisons
    

15.1.5. HASHES句

HASHES句が存在する場合、それはシステムに対して、この演算子に 基づいた結合にハッシュ結合方法を使っても問題が無い事を 伝えます。HASHES句はブール値を返す二項演算子にのみ有効です。 実際には、あるデータ型の等価性を求める演算子であった方が 良いです。

ハッシュ結合の基礎となっている仮定は、結合演算子は左項と右項の値が 同じハッシュコードを持つ時にのみ真を返すことができるということです。 2つの値が異なるハッシュの入れ物に置かれた場合、結合演算子の結果が 必ず偽であるという仮定を、結合は暗黙的に行ない、それらを比べる事を しません。したがって、等価性を表さない演算子にHASHES句を指定することは 全く意味がありません。

実際は、論理的な等価性はあまり十分ではなく、演算子は純粋に ビット単位の等価性を表すものの方が好ましいです。なぜならば、ハッシュ関数は、 ビットの意味は関係なく、メモリ内の値表現を使って計算されるからです。 例えば、時間間隔の等価性はビット単位での等価性ではなく、 間隔の等価性演算子は、二つの時間間隔がその終了時刻が 異なっていた場合でも、期間が同一であれば、その時間間隔は等価である とみなします。これは、間隔フィールドとの間で "=" を使った結合は、 ハッシュ結合を実装した場合とそれ以外で実装した場合とで、異なる結果を もたらすことを意味しています。それは、合うべきペアの多くの部分は異なる値に ハッシュされ、ハッシュ結合時に比較されなくなるために起こります。しかし、 オブティマイザが他種類の結合を使用する事を選んだ場合、等価性演算子が 同一で あるとした全てのペアが見つかります。このような矛盾は 好ましくありませんので、間隔の等価性をハッシュ可能とはしません。

マシンに依存することから、ハッシュ結合が適切な処理を行なわずに 失敗することがあります。例えば、データ型が不要な部分を埋めたビットを持つ 可能性がある構造である場合、その等価性演算子にHASHES句をつけることは 安全ではありません。(他の演算子を不要なビットが常に0になるように 作成している場合は状況が変わる場合があります。)この他の例として、 FLOATデータ型でハッシュ結合を使用するには安全ではない場合があります。 IEEE浮動小数点標準をみたすマシンではマイナス0とプラス0は異なる値 (異なるビット列)となりますが、等価であるものと定義されます。 したがって、浮動小数点の等価性演算子にHASHES句を付けると、 マイナス0とプラス0はハッシュ結合では多分一致されませんが、 他の結合処理では一致するものとされます。

つまり、memcmp()で実装された(できた)等価性演算子にのみにHASHES句を 使用するべきです。

15.1.6. SORT1句とSORT2句

SORT句がある場合、それはシステムに対して指定演算子に基づいた結合に マージ結合方式を使う事ができることを伝えます。どちらか一方がそうであるならば、 両方が指定されなければならなく、指定演算子はあるデータ型の ペアの等価性演算子でなければいけません。また、SORT1句とSORT2句は それぞれ左側、右側用の順序付演算子( '<' 演算子)の名前を示します。

マージ結合は、テーブルの左側、右側を順序良くソートし、 並列にスキャンするという考えに基づいています。したがって、 両データ型は十分に順序付けされている必要があり、結合演算子は ソート順で "同じ場所" にある値のペアをのみを成功したものとする ものである必要があります。実際問題として、これは、結合演算子は 等価性のような振舞いをしなければならないことを意味しています。 左右のデータ型が同じ(または少なくともビット単位での等価)であることが 望ましいとされるハッシュ結合とは異なり、マージ結合は論理的な互換性を持つ 別の2つのデータ型をとることができます。例えば、int2対int4の 等価性演算子はマージ結合が可能です。両方のデータ型を論理的な 互換性を保つ順番にソートする演算子のみが必要です。

マージソート演算子を指定する時は、対象とする演算子と参照された 両演算子はブール値を返さなければいけません。SORT1演算子は、 対象とする演算子の左引数の型と同じデータ型を入力として2つ 持たなければいけません。SORT2演算子は、対象とする演算子の 右引数の型と同じデータ型を入力として2つ持たなければいけません。 (COMMUTATORとNEGATORを使う時と同じように、演算子名は演算子の 指定に十分なものです。他を定義する前に、等価性演算子を定義すると、 システムは仮演算子エントリを作成する事ができます。)

実際には、'='演算子用のSORT句だけを記述し、2つの参照される演算子を常に '<'という名前にしておくべきです。他の名前の演算子を使ってマージ結合を 使用すると、絶望的な混乱を引き起こします。その理由は後に説明します。

マージ結合を行なう演算子には追加的な制約があります。この制約は今のところ CREATE OPERATORで点検されませんが、これが真でなければ、 マージ結合は実行時に失敗する可能性があります。

  • マージ結合が可能な等価性演算子は交替演算子(2つのデータ型が同じならば、 演算子自身、もしくはこれに関連した等価性演算子)を持つ必要があります。

  • 左右とも同じ入力データ型で、かつマージ結合可能な演算子を持つ 順序付演算子'<'と'>'が必要です。この演算子は'<'と'>'という名前で ある必要があり、これらを明示的に指定する方法はないので、 この点については選択の余地がありません。左右のデータ型が異なる場合、 この演算子は互いのSORT演算子と異なるものであることに注意して下さい。 しかし、SORT演算子を使った場合の順序と互換性を持たせなければ、 マージ結合は失敗します。