近和很多人(阿里的,腾讯的,。。。)聊PostgreSQL,发现大家对于PG大的认同点是他的词法分析和语法分析做的非常好,认为是替代Oracle的佳方案。 本人开始接触PostgreSQL开发,便是做的兼容。经过多年的学习,比较认同新手接触PostgreSQL可以先通过系统函数、语法等方式入手。 讨论了许多,正好将自己对于这方面的了解整理一下,也算对自己当年有个交代。 产品的兼容代码由于是商业秘密,在这里无法公开,这里会以PostgreSQL已有内容或开源代码进行介绍实现方式。
按照惯例,先上一张思维导图,以下是本文所讲述内容的概述:
本文首先对兼容做一下阐述,然后对PostgreSQL的系统对象进行介绍,在这里将系统对象拆分为对象、语法以及共享组件。
为什么要做兼容
PostgreSQL本身就有很完善的SQL体系,为什么要做兼容?尤其是为什么要做Oracle的兼容? 主要的原因我认为是Oracle经过多年的市场耕耘,造就了一大批基于Oracle的开发人员。 目前市场上很多应用都是基于Oracle开发的。这就对数据库迁移产生了非常大的阻碍。 国内这几年一直在喊去IOE,但是效果了了。从亚马逊完成Oracle迁移后,进行庆功就很能说明其难度之大。 迁移的不仅仅是数据库,而是整个数据库生态。从应用端开发,到数据库管理,都需要做出改变。 同时,在这里不得不说一下Oracle确确实实在数据库生态方面做了非常多的工作,其他数据库难以望其项背。 为什么要做兼容?尤其是Oracle兼容? 因为目前PostgreSQL在国内的推广程度较小,酒香其实也怕巷子深。可以通过兼容Oracle扩大市场,推广PostgreSQL。 其次,Oracle成功有他的道理,他的生态是非常的完善。向达者学习也是值得去做的。 如果完成了Oracle兼容,那么我们可以做到:
- 更快、更好地应用迁移;
- 完成部分机构的数据库要求;
- 减少新语法的学习成本;
- 完成数据库生态。
兼容级别
那么如果我们要做兼容,那么应该怎么做? 兼容也不一定是一味的完全一模一样,重复造轮子是没有价值的。 我将兼容级别划分为三类:
- 完全相同,使用方法和功能完全一致,终达到用户不需要改动任何应用程序,即可使用;
- 部分相同,使用方法一致,但实现的功能有部分不同,可以针对用户需求完成功能,减少时间消耗;
- 完全不同,实际解决的问题相同,但使用方法不相同,Oracle有很多功能是基于其整体架构的,所以如果想这部分内容,成本非常大,但不如找到根本问题,通过其他方法解决。
这三种级别也不是完全独立的,我们可以有选择性地对兼容内容进行不同级别的兼容开发。比如系统函数我们就可以做的一模一样,而部分类型可以选择部分兼容。
兼容开发方式
我们有以下2种方式开发兼容功能: 完全开发,组织团队,根据需求或者Oracle官方文档(这里吐槽一下,实际使用可能存在比文档展示的方法更多)一一对比,完全实现。 部分开发,基于开源组件二次开发(比如Orafce),或者购买EDB动态库或者兼容代码(资金雄厚的尽可能选择此类)。
系统对象
对象
系统对象主要为下图内容,这些内容基本上已经在orafce中包含了:
系统表
需要增加table.h和table.dat。
- 头文件是表定义以及宏定义,可参看src/include/catalog/pg_class.h。
- dat文件为表内容,这部分可以使用dat初始化赋值或者SQL语句赋值,可参看src/include/catalog/pg_class.dat。
系统视图
可参看src/backend/catalog/system_views.sql:
CREATE VIEW pg_roles AS
SELECT
rolname,
rolsuper,
rolinherit,
rolcreaterole,
rolcreatedb,
rolcanlogin,
rolreplication,
rolconnlimit,
'********'::text as rolpassword,
rolvaliduntil,
rolbypassrls,
setconfig as rolconfig,
pg_authid.oid
FROM pg_authid LEFT JOIN pg_db_role_setting s
ON (pg_authid.oid = setrole AND setdatabase = 0);
内部函数
内部C语言函数实现: 以gram_checker为例,此函数可以对参数进行SQL解析,每次执行1条SQL,成功则返回空,出现错误则提示详细错误信息。 src/backend/utils/adt/gram_checker.c:
Datum //返回类型,PostgreSQL自定义类型
gram_checker(PG_FUNCTION_ARGS) //PG_FUNCTION_ARGS,PostgreSQL自定义宏
{
char *str = PG_GETARG_CSTRING(0); //获取cstring格式的参数
raw_parser(str); //解析SQL
PG_RETURN_NULL(); //返回空值
}
src/include/catalog/pg_proc.dat:
//函数oid,6424
//descr,函数的描述
//proname,函数名称,用于SQL执行使用的函数名
//prorettype,返回类型
//proarg*,参数类型
//prosrc,PostgreSQL内部C语言实现函数名
{ oid => '6424', descr => 'parser SQL',
proname => 'gram_checker', prorettype => 'text',
proarg* => 'cstring', prosrc => 'gram_checker' },
内部SQL函数实现:
//和上面C语言函数不同,这里不需要定义C语言函数,只需在prosrc处定义SQL执行命令即可。
//其次,要定义当前prolang为sql。
{ oid => '1741', descr => 'base 10 logarithm',
proname => 'log', prolang => 'sql', prorettype => 'numeric',
proarg* => 'numeric', prosrc => 'select pg_catalog.log(10, $1)' },
SQL函数实现(可参考: https://www.postgresql.org/docs/current/sql-createfunction.html):
CREATE FUNCTION dup(int) RETURNS TABLE(f1 int, f2 text)
AS $$ SELECT $1, CAST($1 AS text) || ' is text' $$
LANGUAGE SQL;
其他语言函数实现: plpgsql(可参考: https://www.postgresql.org/docs/current/plpgsql-porting.html):
CREATE OR REPLACE FUNCTION cs_fmt_browser_version(v_name varchar,
v_version varchar)
RETURNS varchar AS $$
BEGIN
IF v_version IS NULL THEN
RETURN v_name;
END IF;
RETURN v_name || '/' || v_version;
END;
$$ LANGUAGE plpgsql;
plv8(可参考: https://pgxn.org/dist/plv8/doc/plv8.html):
//安装插件
CREATE EXTENSION plv8;
CREATE EXTENSION plls;
CREATE EXTENSION plcoffee;
//定义函数并执行
CREATE OR REPLACE FUNCTION plv8_test(keys text[], vals text[])
RETURNS text AS $$
var o = {};
for(var i=0; i<keys.length; i++){
o[keys[i]] = vals[i];
}
return JSON.stringify(o);
$$ LANGUAGE plv8 IMMUTABLE STRICT;
SELECT plv8_test(ARRAY['name', 'age'], ARRAY['Tom', '29']);
plv8_test
---------------------------
{"name":"Tom","age":"29"}
(1 row)
操作符
C语言实现: src/include/catalog/pg_operator.dat:
//oid,15
//descr,操作符描述
//oprname,操作符名称,即SQL中使用的名称
//oprcanmerge,可否适用于merge join
//oprcanhash,可否适用于hash join
//oprleft,操作符左边的数据类型
//oprright
//oprresult,操作符返回的数据类型
//oprcom,操作符的转换器
//oprnegate,操作符的逆向操作
//oprcode,操作符调用的函数
//oprrest,操作符的限制选择性估计函数
//oprjoin,操作符的联接选择性估计函数
{ oid => '15', descr => 'equal',
oprname => '=', oprcanmerge => 't', oprcanhash => 't', oprleft => 'int4',
oprright => 'int8', oprresult => 'bool', oprcom => '=(int8,int4)',
oprnegate => '<>(int4,int8)', oprcode => 'int48eq', oprrest => 'eqsel',
oprjoin => 'eqjoinsel' },
SQL实现(可参考: https://www.postgresql.org/docs/12/sql-createoperator.html):
CREATE OPERATOR === (
LEFTARG = box,
RIGHTARG = box,
FUNCTION = area_equal_function,
COMMUTATOR = ===,
NEGATOR = !==,
RESTRICT = area_restriction_function,
JOIN = area_join_function,
HASHES, MERGES
);
类型
C语言定义 src/include/catalog/pg_type.dat:
//typename,类型名称,SQL中使用的名字
//typlen,类型长度,单位字节
//typbyval,typbyval确定内部例程是按值还是按引用传递此类型的值。 如果typlen不是1、2或4(或Datum为8字节的机器上为8),typbyval好为false。 可变长度类型始终通过引用传递。 请注意,即使长度允许值传递,typbyval也可以为false。
//in,out,send,receive函数
//typalign,存储此类型的值时所需的对齐方式。 它适用于磁盘上的存储以及PostgreSQL内部值的大多数表示形式。 当连续存储多个值时(例如,在磁盘上完整行的表示中),将在此类型的数据之前插入填充,以使其开始于指定的边界。 对齐参考是序列中个基准的起点。
{ oid => '20', array_type_oid => '1016',
descr => '~18 digit integer, 8-byte storage',
typname => 'int8', typlen => '8', typbyval => 'FLOAT8PASSBYVAL',
typcategory => 'N', typinput => 'int8in', typoutput => 'int8out',
typreceive => 'int8recv', typsend => 'int8send', typalign => 'd' },
SQL定义(可参考 https://www.postgresql.org/docs/12/sql-createtype.html):
CREATE TYPE box;
CREATE FUNCTION my_box_in_function(cstring) RETURNS box AS ... ;
CREATE FUNCTION my_box_out_function(box) RETURNS cstring AS ... ;
CREATE TYPE box (
INTERNALLENGTH = 16,
INPUT = my_box_in_function,
OUTPUT = my_box_out_function
);
Domain实现(可参考 https://www.postgresql.org/docs/current/sql-createdomain.html):
CREATE DOMAIN us_postal_code AS TEXT
CHECK(
VALUE ~ '^\d{5}$'
OR VALUE ~ '^\d{5}-\d{4}$'
);
语法映射 src/backend/parser/gram.y:
| BIGINT
{
$$ = SystemTypeName("int8");
$$->location = @1;
}
| REAL
{
$$ = SystemTypeName("float4");
$$->location = @1;
}
内部逻辑修改: 将numeric编程int4或int8:
if(typmod_dec == 0)
{
if(typmod_pre <= 4 && typmod_pre >= 0)
typoid = INT2OID;
else if(typmod_pre <= 9 && typmod_pre > 4)
typoid = INT4OID;
else if(typmod_pre <= 18 && typmod_pre > 9)
typoid = INT8OID;
else typoid = NUMERICOID;
隐式转换
C语言实现 src/include/catalog/pg_cast.dat:
//castsource,源类型
//casttarget,目标类型
//castfunc,转换函数
{ castsource => 'int8', casttarget => 'int2', castfunc => 'int2(int8)',
castcontext => 'a', castmethod => 'f' },
SQL定义(参考 https://www.postgresql.org/docs/current/sql-createcast.html):
CREATE CAST (bigint AS int4) WITH FUNCTION int4(bigint) AS ASSIGNMENT;
package
参考: https://github.com/orafce/orafce:
//schema.function,schema即包名,function即包内函数
CREATE FUNCTION plvchr.is_quote(c int)RETURNS BOOLAS $$ SELECT plvchr._is_kind($1, 3);$$LANGUAGE SQL IMMUTABLE STRICT;COMMENT ON FUNCTION plvchr.is_quote(int) IS '';
共享组件
共享组件提供一系列的工具共数据库管理使用,旨在提高数据库管理便宜性。 比如AWR(Automatic Workload Repository),能够提供更多的数据库监控信息,供DBA使用,快速定位、解决数据库问题。
语法
语法,也就是我们常说的SQL,主要涉及到的内容是编译原理。 PostgreSQL使用Flex和Bison两个工具完成的语法实现。 Flex和Bison是Linux下两个用来生成程序的工具,可以处理结构化输入,它们生成的程序分别叫做词法分析器和语法分析器,他们一般结合使用来处理复杂的文件解析工作,在PostgreSQL中主要是用来生成SQL语句的词法和语法分析器。
Flex & Bison
Flex(快速词法分析器生成器)是lex的免费开源软件替代品。它是生成词法分析器(也称为“扫描器”或“词法分析器”)的计算机程序。 它可以利用正则表达式来生成匹配相应字符串的C语言代码,其语法格式基本同Lex相同。经常连同Yacc或GNU Bison一起使用。 Postgresql 中的Flex 文件为scan.l,通过Flex编译为scan.c。
Bison是一种通用解析器生成器,它将带注释的上下文无关文法转换为使用LALR(1)解析器表的确定性LR或广义LR(GLR)解析器 。作为一项实验性功能,Bison还可以生成IELR(1)或规范的LR(1)解析器表。一旦您精通Bison,就可以使用它来开发各种语言解析器,从用于简单台式计算器的语言解析器到复杂的编程语言。 Bison与Yacc向上兼容:所有正确编写的Yacc语法都应与Bison一起使用,而无需进行任何更改。熟悉Yacc的任何人都应该可以轻松使用Bison。您需要精通C或C ++编程才能使用Bison。还支持将Java作为实验功能。 Postgresql 中的Bison 文件为gram.y,通过Bison编译生成gram.h,gram.c。
Flex
Flex由三部分组成
- 定义部分
- %%
- 规则部分
- %%
- 用户附加的C语言部分
请参看:
王硕:Flex介绍Bison
Bison由三部分组成
- 定义部分
- %%
- 规则部分
- %%
- 用户附加的C语言部分
请参看:
王硕:Bison介绍PostgreSQL中语法工作各个文件之间的调用关系:
- scan.l:词法分析器在文件,定义词法结构,编译生成scan.c
- gram.y:语法分析器在文件,定义语法结构,编译生成gram.c和gram.h;
- parser.c:raw_parser函数,主要通过调用生成的base_yyparse函数来实现词法分析和语法分析的工作。
- kwlist.h:SQL关键字定义,注意:关键字名要小写,按照字符串值顺序定义
- kwlookup.h:定义结构体ScanKeyword;
- kwlookup.c:使用kwlist.h初始化关键字数组ScanKeywords,提供ScanKeywordLookup函数
- scanup.c:提供几个词法分析时常用的函数。
下面以SQL,select oid,relname from pg_class where relname='pg_class';为例,说明一下语法解析过程: 首先看一下SQL,
1. select
2. oid,relname
3. from
4. pg_class
5. where
6. relname='pg_class';
我们将SQL转化为语法分析树:
- scanner发现起始位置为‘select’,并确认为关键字SELECT,返回token值与数值;
- parser接收到scanner的返回值,将‘select’移进,发现属于stmtblock->stmtmulti->stmt->SelectStmt->select_no_parens->simple_select;
- scanner发现oid,relname
- 符合移进条件opt_target_list;
- 符合移进条件target_list;
- 其中oid在simple_select符合移进条件target_list->target_el->a_expr->c_expr->columnref->Colld->IDENT,即移进,这时发现符合归约条件target_el,即归约,返回target_el=oid;
- 其中relname在simple_select符合移进条件target_list->target_el->a_expr->c_expr->columnref->Colld->IDENT,即移进,这时发现符合归约条件target_el,即归约,返回target_el=relname;
- target_el=oid,target_el=relname归约符合target_list,即归约;
- target_list符合归约条件opt_target_list,也进行归约。
- scanner发现from,移进from未符合归约条件,继续移进pg_class,符合from_list->table_ref->relation_expr->qualified_name->ColId->IDENT,符合归约条件,则返回from_list;
- scanner发现where
- 符合移进条件where_clause,移进where未符合归约条件,继续移进,发现符合移进条件a_expr;
- 发现符合移进条件a_expr '=' a_expr;
- 继续移进relname,符合a_expr->c_expr->columnref->Colld->IDENT,符合归约条件,则返回a_expr;
- 继续移进‘pg_class’,符合a_expr->c_expr->AexprConst->Sconst->SCONST,符合归约条件,则返回a_expr;
- 符合归约条件,返回where_clause;
- 发现符合归约条件,返回simple_select;
- 发现符合归约条件,返回select_no_parens,SelectStmt,stmt,stmtmulti,stmtblock。
语法能做什么
语法增删改
我们可以在原有的语法体系上增删改语法,增加兼容语法。甚至可以增加多个语法解析器来对不同的数据库进行兼容。
语法模块化
可以考虑将SQL解析器从PostgreSQL代码中单独摘出来作为工具或者库文件,这样可以使用其进行迁移语法检测、异构数据库语法转化、SQL转发。甚至可以基于此进行完成异构数据库语法兼容。
Grammar_checker
近完成了一个语法检测工具,Grammar_checker:
postgres=# \q
[postgres@shawnpc bin]$ ./grammar_checker -S "select * from abcs;"
sqls is select * from abcs;
sqls is ok
[postgres@shawnpc bin]$ ./grammar_checker -S "create table aa();"
sqls is create table aa();
sqls is ok
[postgres@shawnpc bin]$ ./grammar_checker -S "create table aa(id int);"
sqls is create table aa(id int);
sqls is ok
[postgres@shawnpc bin]$ ./grammar_checker -S "select * fsrom abcs;"
sqls is select * fsrom abcs;
grammar_checker: error: syntax error at or near "fsrom"
JIT
利用LLVM对SQL查询计划进行优化。 随着现在存储技术的发展,尤其是内存、ssd硬件的发展,以及存储结构,如列存,如分布式,I/O瓶颈逐渐的在降低,而带来的影响就是cpu的瓶颈来了。很多人可能会问cpu有什么瓶颈?现代数据库为了完成更多的用户操作,有很多的冗余操作。比如我们即便向数据库输入一个常量,数据库还是首先以字符串的形式拿过来,进行处理,经过很多步骤后,确认是常量,然后放入相应位置。这中间就出现了很多人工就能减少的步骤,而数据库或者计算机是不得而知的。 Llvm是一款编译器后端。他能将程序语言转化为自己的中间语言,然后进行优化。 它主要的作用就是减少cpu的指令数量,以及提高cpu cache利用率。 如何理解llvm,如果大家了解java的话,可能就对JIT不陌生了。我们的Java之所以能够一次编程,处处运行,就是因为我们的JDK就是一个解释器。所有的java程序,都经过编译,编译为class文件。这个可以理解为java语言与机器语言的中间语言。而jdk可以将这中间语言重新解释,并优化为机器码,从而进行执行。 pg现有的JIT技术也是相同的方式进行的。不同点在于,这里使用的是llvm作为中间语言的编译和解释、优化再执行。
大体步骤如图所示:
其优化原理以及位置如下:
它是通过LLVM将查询计划转化为LLVM中间语言,然后对中间语言进行优化,减少冗余的指令集并提高cpu Cache的利用率。
总结
以上为我初工作的知识总结,希望对初学PostgreSQL的人有所帮助。