在上一篇關於編寫Postgres Extensions的文章中,咱們介紹了擴展PostgresQL的基礎知識。如今是有趣的部分來了——開發咱們本身的類型。html
最好不要急於複製和粘貼本文中的代碼。文中的代碼有一些嚴重的bug,這些bug是爲了說明解釋的目的而故意留下的。若是您正在尋找可用於生產的base36
類型定義,請查看這裏。python
咱們須要的是一個用於存儲和檢索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)
讓咱們重寫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(參見第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函數(參見第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(第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調試代碼,以及如何經過正確的測試來避免這些錯誤。