Chapter 6. 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 とその他のオプティマイザへのヒントについての詳細は後 述します。

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

著者: Tom Lane 氏

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

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

COMMUTATOR

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

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

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

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

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

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 は挿入されることがありますので、これは 想像以上に多く起こります。

否定子のペアは、上述の交代演算子のペアで説明したものと同じ方法で 定義されます。

RESTRICT

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

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

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

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

非常に高い、または、非常に低い選択性をもつ演算子に、本当に同一また は不同でない場合でも、 eqsel か neqsel を使わないでおく事もできます。 例えば、正規表現比較演算子( ~ 、 ~* など)は、テーブルのエントリの 小部分にのみに合うものと仮定して、eqselを使用します。

JOIN

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

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

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

	eqjoinsel	for =
	neqjoinsel	for <>
	intltjoinsel	for < or <=
	intgtjoinsel	for > or >=
    

HASHES

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

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

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

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

最後に、おそらく memcmp() で実装された(できた)等価性演算子にの み HASHES を使うべきです。

SORT1 and SORT2

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

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

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

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

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

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

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