在上一篇关于编写Postgres Extensions的文章中,我们介绍了扩展PostgresQL的基础知识。现在是有趣的部分来了——开发我们自己的类型。

一个小小的免责声明

最好不要急于复制和粘贴本文中的代码。文中的代码有一些严重的bug,这些bug是为了说明解释的目的而故意留下的。如果您正在寻找可用于生产的base36类型定义,请查看这里

复习一下base36

我们需要的是一个用于存储和检索base36数字的base36数据类型的可靠实现。我们已经为扩展创建了基本框架,包括base36、controler和Makefile,您可以在专门用于本系列博客文章的GitHub repo中找到它们。您可以查看我们在第1部分中得到的结果,本文中的代码可以在第2部分分支中找到。

文件名:base36.control

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

文件名:Makefile

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脚本文件,以显示我们自己的数据类型

文件名: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!

现在我们可以在数据库中存储和检索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调试代码,以及如何通过正确的测试来避免这些错误。

编写Postgres扩展之二:类型和运算符的更多相关文章

  1. JavaScript-基础类型和运算符

    JavaScript-基础类型和运算符 P02.稍微了解 1.js代码需要编写到script标签中 <script type="text/javascript"> 此处 ...

  2. 编写Postgres扩展之五:代码组织和版本控制

    原文:http://big-elephants.com/2015-11/writing-postgres-extensions-part-v/ 编译:Tacey Wong 在关于编写Postgres扩 ...

  3. 编写Postgres扩展之四:测试

    原文:http://big-elephants.com/2015-11/writing-postgres-extensions-part-iv/ 编译:http://big-elephants.com ...

  4. 编写Postgres扩展之一:基础

    原文:http://big-elephants.com/2015-10/writing-postgres-extensions-part-i/ 编译:Tacey Wong Postgres提供了广泛的 ...

  5. Python笔记004-Python最基本内置数据类型和运算符

    第二章(1)Python编程基础概念 1. 最基本内置数据类型和运算符 每个对象都有类型,Python 中最基本的内置数据类型: 1. 整数 整数,2345 ,10 ,50 2. 浮点型 小数,3.1 ...

  6. 编写Postgres扩展之三:调试

    原文:http://big-elephants.com/2015-10/writing-postgres-extensions-part-iii/ 编译:Tacey Wong 在上一篇关于编写Post ...

  7. JAVA基础2----数据类型和运算符

    Java数据类型 1.基本数据类型 整数:byte/short/int/long byte:-128~127 (1个字节) short:-2^15~2^15-1 (2个字节) int(默认类型):-2 ...

  8. Python 变量类型和运算符

    -*- coding:utf-8 -*- ''' if语法 if conditon: [tab键] command [tab键] command ... else: [tab键] command [t ...

  9. shell变量类型和运算符

    一.shell变量的应用 1.shell变量的种类 ①用户自定义变量:由用户自己定义,修改和使用 ②预定义变量:bash预定义的特殊变量,不能直接修改 ③位置变量:通过命令行给程序传递执行参数 二.变 ...

随机推荐

  1. Flutter -------- Drawer侧滑

    侧滑菜单在安卓App里面非常常见 抽屉通常与Scaffold.drawer属性一起使用.抽屉的子项通常是ListView,其第一个子项是DrawerHeader ,它显示有关当前用户的状态信息.其余的 ...

  2. leetcode 402. Remove K Digits 、321. Create Maximum Number

    402. Remove K Digits https://www.cnblogs.com/grandyang/p/5883736.html https://blog.csdn.net/fuxuemin ...

  3. realsense 图片与点云数据采集

  4. C++构造函数以及何时被调用

    using namespace std; class A { public: A() { cout << "默认无参构造函数" << endl; } #if ...

  5. Python - Django - ORM 分组查询补充

    单表查询: models.py: from django.db import models class Employee(models.Model): name = models.CharField( ...

  6. Python - Django - 模板语言之自定义过滤器

    自定义过滤器的文件: 在 app01 下新建一个 templatetags 的文件夹,然后创建 myfilter.py 文件 这个 templatetags 名字是固定的,myfilter 是自己起的 ...

  7. Python - Django - 模板语言之变量

    前言: 在 Django 模板语言中变量用 {{ }},逻辑用 {% %} 在 urls.py 中添加对应关系 from django.conf.urls import url from django ...

  8. Delphi XE中使用dbExpress连接MySQL数据库疑难问题解决

    Delphi IDE中包含一个Data Explorer的组件,如下图所示: 该组件基于dbExpress(包含TSQLConnection.TSQLDataSet.TSQLQuery.TSQLSto ...

  9. echars 3.0 去掉柱状图阴影用什么属性

    原图展示: 效果图展示: 在代码中注释掉这段 // tooltip : { // trigger: 'axis', // axisPointer : { // 坐标轴指示器,坐标轴触发有效 // ty ...

  10. LintCode: coins in a line I

    有 n 个硬币排成一条线.两个参赛者轮流从右边依次拿走 1 或 2 个硬币,直到没有硬币为止.拿到最后一枚硬币的人获胜. 请判定 第一个玩家 是输还是赢? n = 1, 返回 true.n = 2, ...