編寫Postgres擴展之二:類型和運算符


在上一篇關於編寫Postgres Extensions的文章中,咱們介紹了擴展PostgresQL的基礎知識。如今是有趣的部分來了——開發咱們本身的類型。html

一個小小的免責聲明

最好不要急於複製和粘貼本文中的代碼。文中的代碼有一些嚴重的bug,這些bug是爲了說明解釋的目的而故意留下的。若是您正在尋找可用於生產的base36類型定義,請查看這裏python

複習一下base36

咱們須要的是一個用於存儲和檢索base36數字的base36數據類型的可靠實現。咱們已經爲擴展建立了基本框架,包括base3六、controler和Makefile,您能夠在專門用於本系列博客文章的GitHub repo中找到它們。您能夠查看咱們在第1部分中獲得的結果,本文中的代碼能夠在第2部分分支中找到。git

文件名:base36.controlgithub

# base36 extension
comment = 'base36 datatype'
default_version = '0.0.1'
relocatable = true

文件名:Makefilesql

EXTENSION = base36              # 擴展名稱
DATA      = base36--0.0.1.sql   # 用於安裝的腳本文件
REGRESS   = base36_test         # 咱們的測試腳本文件(沒有後綴名)
MODULES   = base36              # 咱們要構建的C模塊文件

# Postgres build stuff
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

Postgres中的自定義數據類型

讓咱們重寫SQL腳本文件,以顯示咱們本身的數據類型shell

文件名:base36-0.0.1.sql數據庫

-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "CREATE EXTENSION base36" to load this file. \quit

CREATE FUNCTION base36_in(cstring)
RETURNS base36
AS '$libdir/base36'
LANGUAGE C IMMUTABLE STRICT;

CREATE FUNCTION base36_out(base36)
RETURNS cstring
AS '$libdir/base36'
LANGUAGE C IMMUTABLE STRICT;

CREATE TYPE base36 (
  INPUT          = base36_in,
  OUTPUT         = base36_out,
  LIKE           = integer
);

這是在Postgres中建立基類型所需的最低要求:咱們須要輸入和輸出兩個函數,它們告訴Postgres如何將輸入文本轉換爲內部表示(base36 in),而後再從內部表示轉換爲文本(base36 out)。咱們還須要告訴Postgres將咱們的類型視爲integer。這也能夠經過在類型定義中指定這些附加參數來實現,以下例所示:服務器

INTERNALLENGTH = 4,     -- use 4 bytes to store data
ALIGNMENT      = int4,  -- align to 4 bytes
STORAGE        = PLAIN, -- always store data inline uncompressed (not toasted)
PASSEDBYVALUE           -- pass data by value rather than by reference

如今咱們來修改C語言部分:
文件名:base36.c框架

#include "postgres.h"
#include "fmgr.h"
#include "utils/builtins.h"

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(base36_in);
Datum
base36_in(PG_FUNCTION_ARGS)
{
    long result;
    char *str = PG_GETARG_CSTRING(0);
    result = strtol(str, NULL, 36);
    PG_RETURN_INT32((int32)result);
}

PG_FUNCTION_INFO_V1(base36_out);
Datum
base36_out(PG_FUNCTION_ARGS)
{
    int32 arg = PG_GETARG_INT32(0);
    if (arg < 0)
        ereport(ERROR,
            (
             errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
             errmsg("negative values are not allowed"),
             errdetail("value %d is negative", arg),
             errhint("make it positive")
            )
        );
    char base36[36] = "0123456789abcdefghijklmnopqrstuvwxyz";

    /* max 6 char + '\0' */
    char *buffer        = palloc(7 * sizeof(char));
    unsigned int offset = 7 * sizeof(char);
    buffer[--offset]    = '\0';

    do {
        buffer[--offset] = base36[arg % 36];
    } while (arg /= 36);

    PG_RETURN_CSTRING(&buffer[offset]);
}

咱們基本上只是重複使用base36_encode函數做爲咱們的OUTPUT並添加了INPUT解碼功能 - So Easy!ide

如今咱們能夠在數據庫中存儲和檢索base36數字。 讓咱們構建並測試它。

make clean && make && make install
test=# CREATE TABLE base36_test(val base36);
CREATE TABLE
test=# INSERT INTO base36_test VALUES ('123'), ('3c'), ('5A'), ('zZz');
INSERT 0 4
test=# SELECT * FROM base36_test;
 val
-----
 123
 3c
 5a
 zzz
(4 rows)

直到如今一切正常。讓咱們對輸出進行排序。

test=# SELECT * FROM base36_test ORDER BY val;
ERROR:  could not identify an ordering operator for type base36
LINE 1: SELECT * FROM base36_test ORDER BY val;
                                           ^
HINT:  Use an explicit ordering operator or modify the query.

嗯……看來咱們漏掉了什麼。

運算符

請記住,咱們正在處理一個徹底空白原始的數據類型。爲了進行排序,咱們須要定義數據類型的實例小於另外一個實例、大於另外一個實例或兩個實例相等的含義。

這不該該太奇怪 - 實際上,它相似於如何在Ruby類中包含Enumerable mixin或者在Golang類型中實現sort.Interface來引入對象的排序規則。(或者對於一個python對象實現__eq__、__lt__等魔法方法,sort函數實現key-lamda)

讓咱們將比較函數和操做符添加到SQL腳本中。

文件名:base36–0.0.1.sql

-- type definition omitted

CREATE FUNCTION base36_eq(base36, base36)
RETURNS boolean LANGUAGE internal IMMUTABLE AS 'int4eq';

CREATE FUNCTION base36_ne(base36, base36)
RETURNS boolean LANGUAGE internal IMMUTABLE AS 'int4ne';

CREATE FUNCTION base36_lt(base36, base36)
RETURNS boolean LANGUAGE internal IMMUTABLE AS 'int4lt';

CREATE FUNCTION base36_le(base36, base36)
RETURNS boolean LANGUAGE internal IMMUTABLE AS 'int4le';

CREATE FUNCTION base36_gt(base36, base36)
RETURNS boolean LANGUAGE internal IMMUTABLE AS 'int4gt';

CREATE FUNCTION base36_ge(base36, base36)
RETURNS boolean LANGUAGE internal IMMUTABLE AS 'int4ge';

CREATE FUNCTION base36_cmp(base36, base36)
RETURNS integer LANGUAGE internal IMMUTABLE AS 'btint4cmp';

CREATE FUNCTION hash_base36(base36)
RETURNS integer LANGUAGE internal IMMUTABLE AS 'hashint4';

CREATE OPERATOR = (
  LEFTARG = base36,
  RIGHTARG = base36,
  PROCEDURE = base36_eq,
  COMMUTATOR = '=',
  NEGATOR = '<>',
  RESTRICT = eqsel,
  JOIN = eqjoinsel,
  HASHES, MERGES
);

CREATE OPERATOR <> (
  LEFTARG = base36,
  RIGHTARG = base36,
  PROCEDURE = base36_ne,
  COMMUTATOR = '<>',
  NEGATOR = '=',
  RESTRICT = neqsel,
  JOIN = neqjoinsel
);

CREATE OPERATOR < (
  LEFTARG = base36,
  RIGHTARG = base36,
  PROCEDURE = base36_lt,
  COMMUTATOR = > ,
  NEGATOR = >= ,
  RESTRICT = scalarltsel,
  JOIN = scalarltjoinsel
);

CREATE OPERATOR <= (
  LEFTARG = base36,
  RIGHTARG = base36,
  PROCEDURE = base36_le,
  COMMUTATOR = >= ,
  NEGATOR = > ,
  RESTRICT = scalarltsel,
  JOIN = scalarltjoinsel
);

CREATE OPERATOR > (
  LEFTARG = base36,
  RIGHTARG = base36,
  PROCEDURE = base36_gt,
  COMMUTATOR = < ,
  NEGATOR = <= ,
  RESTRICT = scalargtsel,
  JOIN = scalargtjoinsel
);

CREATE OPERATOR >= (
  LEFTARG = base36,
  RIGHTARG = base36,
  PROCEDURE = base36_ge,
  COMMUTATOR = <= ,
  NEGATOR = < ,
  RESTRICT = scalargtsel,
  JOIN = scalargtjoinsel
);

CREATE OPERATOR CLASS btree_base36_ops
DEFAULT FOR TYPE base36 USING btree
AS
        OPERATOR        1       <  ,
        OPERATOR        2       <= ,
        OPERATOR        3       =  ,
        OPERATOR        4       >= ,
        OPERATOR        5       >  ,
        FUNCTION        1       base36_cmp(base36, base36);

CREATE OPERATOR CLASS hash_base36_ops
    DEFAULT FOR TYPE base36 USING hash AS
        OPERATOR        1       = ,
        FUNCTION        1       hash_base36(base36);

哇…太多了。對其進行分解:首先,咱們爲每個比較運算符定義了一個比較函數進行賦能(<, <=, =, >= 和 >)。而後咱們將它們放在一個操做符類中,這個操做符類將使咱們可以在新的數據類型上建立索引。

對於函數自己,咱們能夠簡單地爲integer類型重用相應的內置函數:int4eq, int4ne, int4lt, int4le, int4gt, int4ge, btint4cmp 和 hashint4。

如今讓咱們老看看運算符定義。

每個運算符都有一個左參數(LEFTARG),一個右參數(RIGHTARG)和 一個函數(PROCEDURE)。

所以,若是咱們進行下面的操做:

SELECT 'larg'::base36 < 'rarg'::base36;
 ?column?
----------
 t
(1 row)

Postgresql將會使用base36_lt函數暨base36_lt('larg','rarg')進行對兩個base36類型的數據進行比較。

COMMUTATOR 和 NEGATOR

每一個運算符還有一個COMMUTATOR和一個NEGATOR(參見第52-53行)。查詢規劃器使用它們進行優化。commutator是應該用於表示相同結果可是翻轉參數的運算符。因爲對於全部可能的值x和y ,(x < y) = (y > x),因此操做符>是操做符<的commutator。同理,操做符 <是操做符> 的commutator。否認器是否認運算符布爾結果的運算符。也就是說,對於全部可能的值x和y, (x < y) = NOT(x >= y)。

爲何這很重要呢?假設您已經索引了val列:

EXPLAIN SELECT * FROM base36_test where 'c1'::base36 > val;
                                           QUERY PLAN
-------------------------------------------------------------------------------------------------
 Index Only Scan using base36_test_val_idx on base36_test  (cost=0.42..169.93 rows=5000 width=4)
   Index Cond: (val < 'c1'::base36)
(2 rows)

能夠看到,爲了可以使用索引,Postgres必須將查詢從'c1'::base36 > val重寫爲val < 'c1'::base36。

否認也是如此。

base36_test=# explain SELECT * FROM base36_test where NOT val > 'c1';
                                           QUERY PLAN
-------------------------------------------------------------------------------------------------
 Index Only Scan using base36_test_val_idx on base36_test  (cost=0.42..169.93 rows=5000 width=4)
   Index Cond: (val <= 'c1'::base36)
(2 rows)

這裏NOT val>'c1':: base36被重寫爲val <='c1':: base36。

最後你能夠看到它會將NOT'c1':: base36 <val重寫爲val <='c1'::

base36_test=# explain SELECT * FROM base36_test where NOT 'c1' < val;
                                           QUERY PLAN
-------------------------------------------------------------------------------------------------
 Index Only Scan using base36_test_val_idx on base36_test  (cost=0.42..169.93 rows=5000 width=4)
   Index Cond: (val <= 'c1'::base36)
(2 rows)

所以,雖然在自定義Postgres類型定義中並不嚴格要求COMMUTATOR和NEGATOR子句,但若是沒有它們,則沒法進行上述重寫。 所以,各個查詢將不會使用索引,而且在大多數狀況下會失去性能。

RESTRICT 和 JOIN

幸運的是,咱們不須要編寫本身的RESTRICT函數(參見第54-55行),能夠簡單地使用它:

eqsel for =
neqsel for <>
scalarltsel for < or <=
scalargtsel for > or >=

這些是限制選擇性估計函數,它給Postgres一個提示,即在給定常量做爲右參數的狀況下,有多少行知足WHERE子句。若是常數是左邊的參數,咱們能夠用commutator把它翻轉到右邊。

你可能已經知道,當你或autovacuum守護程序運行ANALYZE時,Postgres會收集每一個表的一些統計信息。你還能夠在pg stats視圖中查看這些統計數據。

SELECT * FROM pg_stats WHERE tablename = 'base36_test';

全部估計函數都是給出介於0和1之間的值,表示基於這些統計的行的估計分數。這一點很是重要,由於一般=操做符知足的行數少於<>操做符。因爲在命名和定義操做符方面相對比較自由,因此須要說明它們是如何工做的。

若是你真的想知道估算函數是什麼樣子的,請看源代碼。免責聲明:你的眼睛可能會開始流血。

所以,咱們不須要編寫本身的JOIN選擇性估計函數,這很是好。這個是用於多表join查詢的,但本質上是同樣的:它估計操做將返回多少行以最終決定使用哪一個可能的計劃(即哪一個鏈接順序)。

因此,若是你有:

ELECT * FROM table1
JOIN table2 ON table1.c1 = table2.c1
JOIN table3 ON table2.c1 = table2.c1

這類的查詢,這裏表3只有幾行,而表1和表2很是大。所以,首先聯接表3,積累一些行,而後聯接其餘表是有意義的。

HASHES 和 MERGES

對於等式運算符,咱們還定義參數HASHES和MERGES(第35行)。這樣作就是告訴Postgres,使用此函數進行散列分別合併鏈接操做是合適的。爲了使散列鏈接真正起做用,咱們還須要定義一個散列函數並將它們放在一個運算符類中。您能夠在PostgreSQL文檔中進一步閱讀有關不一樣Operator Optimization子句的內容。

更多內容

到目前爲止,你已經瞭解瞭如何使用INPUT和OUTPUT函數實現基本數據類型。最重要的是,咱們經過重用Postgres內部功能來添加比較運算符的。這容許咱們對錶進行排序並使用索引。

可是,若是你按上面的步驟在計算機上的進行實現,可能會發現上面提到的EXPLAIN命令不起做用:

# EXPLAIN SELECT * FROM base36_test where 'c1'::base36 > val;
server closed the connection unexpectedly
  This probably means the server terminated abnormally
  before or while processing the request.
The connection to the server was lost. Attempting reset: Failed.
Time: 275,327 ms
!>

那是由於咱們作了最糟糕的事情:在某些狀況下,咱們的代碼會致使整個服務器崩潰。

在下一篇文章中,咱們將看到如何使用LLDB調試代碼,以及如何經過正確的測試來避免這些錯誤。

相關文章
相關標籤/搜索