绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
PostgreSQL之系统对象介绍
2020-05-19 17:35:49

近和很多人(阿里的,腾讯的,。。。)聊PostgreSQL,发现大家对于PG大的认同点是他的词法分析和语法分析做的非常好,认为是替代Oracle的佳方案。 本人开始接触PostgreSQL开发,便是做的兼容。经过多年的学习,比较认同新手接触PostgreSQL可以先通过系统函数、语法等方式入手。 讨论了许多,正好将自己对于这方面的了解整理一下,也算对自己当年有个交代。 产品的兼容代码由于是商业秘密,在这里无法公开,这里会以PostgreSQL已有内容或开源代码进行介绍实现方式。

按照惯例,先上一张思维导图,以下是本文所讲述内容的概述:

本文首先对兼容做一下阐述,然后对PostgreSQL的系统对象进行介绍,在这里将系统对象拆分为对象、语法以及共享组件。

为什么要做兼容

PostgreSQL本身就有很完善的SQL体系,为什么要做兼容?尤其是为什么要做Oracle的兼容? 主要的原因我认为是Oracle经过多年的市场耕耘,造就了一大批基于Oracle的开发人员。 目前市场上很多应用都是基于Oracle开发的。这就对数据库迁移产生了非常大的阻碍。 国内这几年一直在喊去IOE,但是效果了了。从亚马逊完成Oracle迁移后,进行庆功就很能说明其难度之大。 迁移的不仅仅是数据库,而是整个数据库生态。从应用端开发,到数据库管理,都需要做出改变。 同时,在这里不得不说一下Oracle确确实实在数据库生态方面做了非常多的工作,其他数据库难以望其项背。 为什么要做兼容?尤其是Oracle兼容? 因为目前PostgreSQL在国内的推广程度较小,酒香其实也怕巷子深。可以通过兼容Oracle扩大市场,推广PostgreSQL。 其次,Oracle成功有他的道理,他的生态是非常的完善。向达者学习也是值得去做的。 如果完成了Oracle兼容,那么我们可以做到:

  1. 更快、更好地应用迁移;
  2. 完成部分机构的数据库要求;
  3. 减少新语法的学习成本;
  4. 完成数据库生态。

兼容级别

那么如果我们要做兼容,那么应该怎么做? 兼容也不一定是一味的完全一模一样,重复造轮子是没有价值的。 我将兼容级别划分为三类:

  1. 完全相同,使用方法和功能完全一致,终达到用户不需要改动任何应用程序,即可使用;
  2. 部分相同,使用方法一致,但实现的功能有部分不同,可以针对用户需求完成功能,减少时间消耗;
  3. 完全不同,实际解决的问题相同,但使用方法不相同,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函数实现(可参考: postgresql.org/docs/cur):

CREATE FUNCTION dup(int) RETURNS TABLE(f1 int, f2 text)
    AS $$ SELECT $1, CAST($1 AS text) || ' is text' $$
    LANGUAGE SQL;

其他语言函数实现: plpgsql(可参考: postgresql.org/docs/cur):

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(可参考: pgxn.org/dist/plv8/doc/):

//安装插件
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实现(可参考: postgresql.org/docs/12/):

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定义(可参考 postgresql.org/docs/12/):

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实现(可参考 postgresql.org/docs/cur):

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定义(参考 postgresql.org/docs/cur):

CREATE CAST (bigint AS int4) WITH FUNCTION int4(bigint) AS ASSIGNMENT;

package

参考: github.com/orafce/orafc

//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介绍zhuanlan.zhihu.com图标

Bison

Bison由三部分组成

  • 定义部分
  • %%
  • 规则部分
  • %%
  • 用户附加的C语言部分

请参看:

王硕:Bison介绍zhuanlan.zhihu.com图标

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转化为语法分析树:

  1. scanner发现起始位置为‘select’,并确认为关键字SELECT,返回token值与数值;
  2. parser接收到scanner的返回值,将‘select’移进,发现属于stmtblock->stmtmulti->stmt->SelectStmt->select_no_parens->simple_select;
  3. scanner发现oid,relname
    1. 符合移进条件opt_target_list;
    2. 符合移进条件target_list;
    3. 其中oid在simple_select符合移进条件target_list->target_el->a_expr->c_expr->columnref->Colld->IDENT,即移进,这时发现符合归约条件target_el,即归约,返回target_el=oid;
    4. 其中relname在simple_select符合移进条件target_list->target_el->a_expr->c_expr->columnref->Colld->IDENT,即移进,这时发现符合归约条件target_el,即归约,返回target_el=relname;
    5. target_el=oid,target_el=relname归约符合target_list,即归约;
    6. target_list符合归约条件opt_target_list,也进行归约。
  4. scanner发现from,移进from未符合归约条件,继续移进pg_class,符合from_list->table_ref->relation_expr->qualified_name->ColId->IDENT,符合归约条件,则返回from_list;
  5. scanner发现where
    1. 符合移进条件where_clause,移进where未符合归约条件,继续移进,发现符合移进条件a_expr;
    2. 发现符合移进条件a_expr '=' a_expr;
    3. 继续移进relname,符合a_expr->c_expr->columnref->Colld->IDENT,符合归约条件,则返回a_expr;
    4. 继续移进‘pg_class’,符合a_expr->c_expr->AexprConst->Sconst->SCONST,符合归约条件,则返回a_expr;
    5. 符合归约条件,返回where_clause;
  6. 发现符合归约条件,返回simple_select;
  7. 发现符合归约条件,返回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的人有所帮助。

分享好友

分享这个小栈给你的朋友们,一起进步吧。

华山论剑
创建时间:2019-02-22 18:53:00
没了烟火气,人生就是一段孤独的旅程·····于是,在ITPUB,我们以武论英雄!
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

栈主、嘉宾

查看更多
  • 栈栈
    栈主
  • ?
    嘉宾

小栈成员

查看更多
  • u_9a3ed7a37f8e4a
  • daisyplay
  • boss_ch
  • Jack2k
戳我,来吐槽~