在上一篇关于编写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调试代码,以及如何经过正确的测试来避免这些错误。