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

相关文章
相关标签/搜索