在PostgreSQL自定義一個“優雅”的type


是的,又是我,不要臉的又來混經驗了。我們知道PostgreSQL是一個高度可擴展的數據庫,這次我聊聊如何在PostgreSQL里創建一個優雅的type,如何理解優雅?大概就是不僅僅是type本身,其它相關的“服務”都得跟上,要像數據庫自帶的type一樣想怎么用怎么用。

好的,我們開始。

1. CREATE TYPE

PostgreSQL能夠被擴展成支持新的數據類型。這一節我們先說說如何定義新的基本類型,這里的type是被定義在SQL語言層面之下的數據類型。創建一種新的基本類型要求使用低層語言(通常是 C)實現在該類型上操作的函數。

例子來自於源代碼src/tutorial目錄下的complex.sql和complex.c。運行這些例子的指令可以在該目錄的README文件中找到。

CREATE TYPE的語法在這里:http://www.postgres.cn/docs/9.5/sql-createtype.html

在PostgreSQL中,一種用戶定義的類型必須總是具有輸入和輸出函數。這些函數決定該類型如何出現在字符串中(用於用戶輸入或者對用戶的輸出)以及如何在內存中組織該類型.

關於以上提到的這些,我先不要臉的畫一個圖吧:

outside(screen)       inside(disk file)    
 |  ------------------------->  |
 | complex_in                   |
 |                              |
 |                              |
 |  <-------------------------  |
 |                  complex_out |

比如我們要創建一個復數類型complex,它的C語言描述如下:

typedef struct Complex {
    double      x;
    double      y;
} Complex;

在用C語言實現時我們將需要讓它成為一種傳引用類型,因為它沒辦法放到一個單一的Datum值中(因為Datum里面只能放單一的基本數據類型)。

接着為美觀易懂起見,我們選擇字符串形式的"(x,y)作為該類型的外部字符串表達"。

也就是說,我們用"(x,y)"表示一個complex,並且在使用時我們這樣書寫,在查詢時,數據庫返回的也是這樣的字符串。

我們寫一個簡單的輸入函數吧:

Datum
complex_in(PG_FUNCTION_ARGS)
{
    char       *str = PG_GETARG_CSTRING(0);
    double      x,
                y;
    Complex    *result;

    if (sscanf(str, " ( %lf , %lf )", &x, &y) != 2)
        ereport(ERROR,
                (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
                 errmsg("invalid input syntax for complex: \"%s\"",
                        str)));

    result = (Complex *) palloc(sizeof(Complex));
    result->x = x;
    result->y = y;
    PG_RETURN_POINTER(result);
}

再來一個輸出函數

PG_FUNCTION_INFO_V1(complex_out);

Datum
complex_out(PG_FUNCTION_ARGS)
{
    Complex    *complex = (Complex *) PG_GETARG_POINTER(0);
    char       *result;

    result = psprintf("(%g,%g)", complex->x, complex->y);
    PG_RETURN_CSTRING(result);
}

函數寫完了,也編譯成動態庫了(細節可以看看我之前的文章)。我們的數據庫怎么知道去調用這些函數呢?
我們在SQL中這樣定義函數(filename就是你的文件路徑了):

CREATE FUNCTION complex_in(cstring)
    RETURNS complex
    AS 'filename'
    LANGUAGE C IMMUTABLE STRICT;

CREATE FUNCTION complex_out(complex)
    RETURNS cstring
    AS 'filename'
    LANGUAGE C IMMUTABLE STRICT;

CREATE FUNCTION complex_recv(internal)
   RETURNS complex
   AS 'filename'
   LANGUAGE C IMMUTABLE STRICT;

CREATE FUNCTION complex_send(complex)
   RETURNS bytea
   AS 'filename'
   LANGUAGE C IMMUTABLE STRICT;

可選地,一種用戶定義的類型可以提供二進制輸入和輸出函數。二進制 I/O 通常比文本 I/O 更快但是可移植性更差。與文本 I/O 一樣,定義准確的外部二進制表達是你需要負責的工作。大部分的內建數據類型都嘗試提供一種不依賴機器的二進制表達。

最后,我們可以提供該數據類型的完整定義:

CREATE TYPE complex (
   internallength = 16,
   input = complex_in,
   output = complex_out,
   receive = complex_recv,
   send = complex_send,
   alignment = double
);

到這里你應該在疑惑輸入和輸出函數是如何能被聲明為具有新類型的結果或參數的?因為必須在創建新類型之前創建這兩個函數。這個問題的答案是,新類型應該首先被定義為一種shell type, 它是一種占位符類型,除了名稱和擁有者之外它沒有其他屬性。這可以 通過不帶額外參數的命令CREATE TYPE name做到。然后用 C 寫的 I/O 函數可以 被定義為引用這種 shell type。最后,用帶有完整定義的 CREATE TYPE把該 shell type 替換為一個完全的、合法的類型定義,之后新類型就可以正常使用了

這樣以后,某張系統表里就有新內容了。也就是說,你的type”注冊“好了。

我們已經有了Complex這個數據類型,一旦數據類型存在,我們就能夠聲明額外的函數來提供在該數據類型上有用的操作。不然這個數據實際上就是一個空架子而已,還有一些聚集函數也沒有支持。沒辦法進一步的使用。因此,有必要為它創建一些聚集函數和操作符。
本來是想先介紹操作符的,為行文連貫起見,我先講講創建聚集函數吧。


2. CREATE AGGREGATE

先放官方文檔:http://www.postgres.cn/docs/9.4/xaggr.html

http://www.postgres.cn/docs/9.4/sql-createaggregate.html

以我個人的開發經驗的話,一般寫聚集函數會寫成下面這個形式:

CREATE AGGREGATE xxx (complex)
    (
    	sfunc = xxx_trans,
		stype = internal,
		finalfunc = xxx_final,
		combinefunc = xxx_trans_merge,
		serialfunc  = xxx_serialize,
		deserialfunc = xxx_deserialize,
		parallel = SAFE
	);

我還是以聚集函數avg()為例來講解吧。例如你執行"SELECT avg(a) FROM some_table;",

你求和要依次遍歷每一個a吧?這里的sfunc(狀態轉移函數)就是做遍歷操作的(數據庫對每行都會調用一次sfunc,因此你的sfunc只要寫針對一行的操作);

把每個a的值和遍歷的值的個數要記下來吧?存這個你得弄個結構體啥的吧?這個stype就是指定這個變量類型的;

把所有的行都遍歷完了我們要把總和除以行數才能得到結果吧?finalfunc()就是做這個事情的,它在遍歷之后做最終處理。對這個finalfunc我多說一句,如果你寫的聚集函數類似於sum()這種,遍歷完我就可以得到結果,那么我們就可以不定義finalfunc,但是你的stype必須就是你的函數返回值類型。

有了這三個函數,聚集函數就可以跑起來了。剩下的幾個函數和參數是PostgreSQL在9.6時為支持並行查詢時加入的。還是以執行"SELECT avg(a) FROM some_table;"為例:

並行查詢要支持並行聚集的話,那么我必須要能把每個並行的process的結果合並在一起吧?怎么辦?上combinefunc函數吧,它用來合並兩個"中間狀態";

上面說的我要合並各個process間的數據,那么要保證各個process間可以傳遞數據吧?所以要序列化和反序列化,所以就要寫serialfunc和deserialfunc;

最后你寫了以上這些函數,支持了並行,那么就設置parallel為SAFE,讓數據庫知道你支持了parallel,否則數據庫默認對你這個聚集函數不並行處理。

聚集函數差不多就是這些,聚集函數是可以重載的,然后上面這些函數都寫成針對complex的實現,數據庫就會自己去調用。具體實現細節不想講了,不是本篇的主題(可能會另開一篇細講服務端編程)。

好的。現在再說operator。


3. CREATE OPERATOR

語法在這里:
http://www.postgres.cn/docs/9.5/sql-createoperator.html
http://www.postgres.cn/docs/9.5/xoper.html

CREATE OPERATOR name (
    PROCEDURE = function_name
    [, LEFTARG = left_type ] [, RIGHTARG = right_type ]
    [, COMMUTATOR = com_op ] [, NEGATOR = neg_op ]
    [, RESTRICT = res_proc ] [, JOIN = join_proc ]
    [, HASHES ] [, MERGES ]
)

粗略地說,對於一個我們想要創建的操作符,我們要指定:

操作符名字(name),

一個處理函數(PROCEDURE),

指定它是幾元操作符(LEFTARG,RIGHTARG),

操作符的交換子(如果對於所有可能輸入的 x、y 值, (x A y) 等於 (y B x),我們可以說操作符 A 和 B 互為交換子。)(COMMUTATOR),

求反器(neg_op),

那么對於上面提到的complex類型,我們增加一個"+"操作符吧。

CREATE FUNCTION complex_add(complex, complex)
    RETURNS complex
    AS 'filename', 'complex_add'
    LANGUAGE C IMMUTABLE STRICT;

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

4. CREATE OPERATOR CLASS

有了操作符,我們可以對數據進行一些必要的操作了。我們玩着玩着,數據量就大了,這時候我們很當然的想我們需要一個索引啊。然而一個索引方法的例程並不直接了解它將要操作的數據類型。而是由一個 操作符類標識索引方法。
那么好吧,我們創建一個操作符類,為了索引。我們知道,在創建索引的時候我們是要選擇索引類型的,比如B樹索引,gin索引,gist索引等等。

語法如下:

CREATE OPERATOR CLASS name [ DEFAULT ] FOR TYPE data_type
  USING index_method [ FAMILY family_name ] AS
  {  OPERATOR strategy_number operator_name [ ( op_type, op_type ) ] [ FOR SEARCH | FOR ORDER BY sort_family_name ]
   | FUNCTION support_number [ ( op_type [ , op_type ] ) ] function_name ( argument_type [, ...] )
   | STORAGE storage_type
  } [, ... ]

我們可以從語法里看到:這個操作符類是針對某一個type指定為某種index_method時服務的,也就是說我比如這次為complex指定了b-tree索引的操作符類,那么我要是想用complex類型的hash索引,對不起我們得為hash索引再建立一個操作符類

下面我依次來說明下CREATE OPERATOR CLASS語法中"{"和"}"之間的文字的意思。

首先對於OPERATOR:

OPERATOR strategy_number operator_name [ ( op_type, op_type ) ] [ FOR SEARCH | FOR ORDER BY sort_family_name ]

與一個操作符類關聯的操作符通過"策略號"(strategy_number)標識,它被用來標識每個操作符在其操作符類中的語義。例如,B-樹在鍵上施行了一種嚴格的順序(較小到較大),因此"小於"和"大於等於" 這樣的操作符就是 B-樹所感興趣的。因為PostgreSQL允許用戶定義操作符, PostgreSQL不能看着一個操作符(如<和>=)的名字並且說出它是哪一種比較。

取而代之的是,索引方法定義了一個"策略"集合, 它們可以被看成是廣義的操作符。每一個操作符類會說明對於一種特定 的數據類型究竟是哪個實際的操作符對應於每一種策略以及該索引語義的解釋。

我們可以在代碼里看到這些策略號:

src/include/access/stratnum.h

我們先看下b-tree的策略號。

假如我們給complex寫了很多操作符,其中我們寫了個"@<"這個操作符,我們也不知道這個操作符是干什么鬼的,但是我們知道"a @< b"表達的"a小於b",那么我們就知道他針對b-tree索引的策略號是1,那我們這么寫:

OPERATOR  1   @<(complex, complex)

下面再來說FUNCTION:

我們說OPERATOR有strategy_number,那么對於FUNCTION,他類似的也有support_number:

也就是說你得實現這些函數,索引才能正常工作:

FUNCTION  1 complex_abs_cmp(complex, complex)

最后,storage_type是實際存儲在索引中的數據類型。通常這和列數據類型相同,但是有些索引方法(當前有 GiST 、GIN 和 BRIN)允許它們不同。 除非索引方法允許使用不同的類型,STORAGE子句必須被省略。那我們也省略吧。

有關strategy_number和support_number看這里:
http://www.postgres.cn/docs/9.5/xindex.html

其實到這里,我也可以結束本文了,因為真的差不多寫完了。然而人還是有強迫症的。奈何官方手冊上還有個操作符族。老實說,我目前的應用場景還沒遇到過。算了還是寫上吧,感覺終究躲不過的。

5. CREATE OPERATOR FAMILY

(以下抄的官方手冊)

到目前為止,我們暗地里假設一個操作符類只處理一種數據類型。雖然在 一個特定的索引列中必定只有一種數據類型,但是把被索引列與一種不同 數據類型的值比較的索引操作通常也很有用。還有,如果與一種操作符類 相關的擴數據類型操作符有用,通常情況是其他數據類型也有其自身相關 的操作符類。在相關的類之間建立起明確的聯系會很有用,因為這可以幫 助規划器進行 SQL 查詢優化(尤其是對於 B-樹操作符類,因為規划器包 含了大量有關如何使用它們的知識)。

為了處理這些需求,PostgreSQL 使用了操作符族的概念。 一個操作符族包含一個或者多個操作符類,並且也能包含屬於該族整體而 不屬於該族中任何單一類的可索引操作符和相應的支持函數。我們說這樣的 操作符和函數是"松散地"存在於該族中,而不是被綁定在一個 特定的類中。通常每個操作符類包含單一數據類型的操作符,而跨數據類型 操作符則松散地存在於操作符族中。

(以上抄的官方手冊)

看不懂的話上例子。

CREATE OPERATOR FAMILY integer_ops USING btree;

CREATE OPERATOR CLASS int8_ops
DEFAULT FOR TYPE int8 USING btree FAMILY integer_ops AS
  -- 標准 int8 比較
  OPERATOR 1 < ,
  OPERATOR 2 <= ,
  OPERATOR 3 = ,
  OPERATOR 4 >= ,
  OPERATOR 5 > ,
  FUNCTION 1 btint8cmp(int8, int8) ,
  FUNCTION 2 btint8sortsupport(internal) ;

CREATE OPERATOR CLASS int4_ops
DEFAULT FOR TYPE int4 USING btree FAMILY integer_ops AS
  -- 標准 int4 比較
  OPERATOR 1 < ,
  OPERATOR 2 <= ,
  OPERATOR 3 = ,
  OPERATOR 4 >= ,
  OPERATOR 5 > ,
  FUNCTION 1 btint4cmp(int4, int4) ,
  FUNCTION 2 btint4sortsupport(internal) ;

比如我們有以上int4和int8的操作符類,以及他們共同屬於一個操作符族。

我們知道,雖然int4和int8不是同一種數據類型,但是在一般我們認為安全的情況下,我們還是經常會把他們之間做比較的,比如大於小於相等之類的。對於他們比較的操作,我們擴展重載幾個操作符就好了,讓int4和int8可以相加之類的。但是他們卻不能在索引時被使用,於是這時候我們用上了操作符族:

我們把int4和int8放在一個操作符族下,然后:

ALTER OPERATOR FAMILY integer_ops USING btree ADD

  -- 跨類型比較 int8 vs int4
  OPERATOR 1 < (int8, int4) ,
  OPERATOR 2 <= (int8, int4) ,
  OPERATOR 3 = (int8, int4) ,
  OPERATOR 4 >= (int8, int4) ,
  OPERATOR 5 > (int8, int4) ,
  FUNCTION 1 btint84cmp(int8, int4) ,

  -- 跨類型比較 int4 vs int8
  OPERATOR 1 < (int4, int8) ,
  OPERATOR 2 <= (int4, int8) ,
  OPERATOR 3 = (int4, int8) ,
  OPERATOR 4 >= (int4, int8) ,
  OPERATOR 5 > (int4, int8) ,
  FUNCTION 1 btint48cmp(int4, int8) ,

這樣我們就可以在索引里面支持這樣的比較了。

這篇就是這樣了。下回要不就寫寫服務端編程,或者再開寫postgreSQL的executor的執行源碼分析。

朋友們,下篇文章見吧~

歡迎點贊。。。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM