本开发设计建议约定数据库建模和数据库应用程序开发过程中,应当遵守的设计规范。依据这些规范进行建模,能够更好的契合openGauss的分布式处理架构,输出更高效的业务SQL代码。
本开发设计建议中所陈述的“建议”和“关注”含义如下:
数据库对象命名需要满足约束:非时序表长度不超过63个字节,时序表长度不超过53个字符,以字母或下划线开头,中间字符可以是字母、数字、下划线、$、#。
【建议】避免使用保留或者非保留关键字命名数据库对象。
说明: 可以使用select * from pg_get_keywords()查询openGauss的关键字,或者在关键字章节中查看。
【建议】避免使用双引号括起来的字符串来定义数据库对象名称,除非需要限制数据库对象名称的大小写。数据库对象名称大小写敏感会使定位问题难度增加。
【建议】数据库对象命名风格务必保持统一。
【建议】表对象的命名应该可以表征该表的重要特征。例如,在表对象命名时区分该表是普通表、临时表还是非日志表:
不创建以mlog_和以matviewmap_为前缀的数据库对象。
【建议】非时序表对象命名建议不要超过63字节。如果过该长度内核会对表名进行截断,从而造成和设置值不一致的现象。且在不同字符集下,可能造成字符被截断,出现预期外的字符。
openGauss中可以使用Database和Schema实现业务的隔离,区别在于Database的隔离更加彻底,各个Database之间共享资源极少,可实现连接隔离、权限隔离等,Database之间无法直接互访。Schema隔离的方式共用资源较多,可以通过grant与revoke语法便捷地控制不同用户对各Schema及其下属对象的权限。
openGauss是分布式架构。数据分布在各个DN上。总体上讲,良好的表设计需要遵循以下原则:
【建议】表的存储类型是表定义设计的第一步,客户业务类型是决定表的存储类型的主要因素,表存储类型的选择依据请参考表1。
表 1 表的存储类型及场景
存储类型 |
适用场景 |
---|---|
行存 |
|
列存 |
|
【建议】表的分布方式的选择一般遵循以下原则:
表 2 表的分布方式及使用场景
分布方式 |
描述 |
适用场景 |
---|---|---|
Hash |
表数据通过Hash方式散列到数据库实例中的所有DN上。 |
数据量较大的事实表。 |
Replication |
数据库实例中每一个DN都有一份全量表数据。 |
维度表、数据量较小的事实表。 |
Range |
表数据对指定列按照范围进行映射,分布到对应DN。 |
用户需要自定义分布规则的场景。 |
List |
表数据对指定列按照具体值进行映射,分布到对应DN。 |
用户需要自定义分布规则的场景。 |
当表中的数据量很大时,应当对表进行分区,一般需要遵循以下原则:
典型的分区表定义如下:
复制代码CREATE TABLE staffS_p1
(
staff_ID NUMBER(6) not null,
FIRST_NAME VARCHAR2(20),
LAST_NAME VARCHAR2(25),
EMAIL VARCHAR2(25),
PHONE_NUMBER VARCHAR2(20),
HIRE_DATE DATE,
employment_ID VARCHAR2(10),
SALARY NUMBER(8,2),
COMMISSION_PCT NUMBER(4,2),
MANAGER_ID NUMBER(6),
section_ID NUMBER(4)
)
PARTITION BY RANGE (HIRE_DATE)
(
PARTITION HIRE_19950501 VALUES LESS THAN ('1995-05-01 00:00:00'),
PARTITION HIRE_19950502 VALUES LESS THAN ('1995-05-02 00:00:00'),
PARTITION HIRE_maxvalue VALUES LESS THAN (MAXVALUE)
);
Hash表的分布键选取至关重要,如果分布键选择不当,可能会导致数据倾斜,从而导致查询时,I/O负载集中在部分DN上,影响整体查询性能。因此,在确定Hash表的分布策略之后,需要对表数据进行倾斜性检查,以确保数据的均匀分布。分布键的选择一般需要遵循以下原则:
字段设计时,基于查询效率的考虑,一般遵循以下原则:
【建议】尽量使用高效数据类型。
选择数值类型时,在满足业务精度的情况下,选择数据类型的优先级从高到低依次为整数、浮点数、NUMERIC。
【建议】当多个表存在逻辑关系时,表示同一含义的字段应该使用相同的数据类型。
【建议】对于字符串数据,建议使用变长字符串数据类型,并指定最大长度。请务必确保指定的最大长度大于需要存储的最大字符数,避免超出最大长度时出现字符截断现象。除非明确知道数据类型为固定长度字符串,否则,不建议使用CHAR(n)、BPCHAR(n)、NCHAR(n)、CHARACTER(n)。
关于字符串类型的详细说明,请参见常用字符串类型介绍。
在进行字段设计时,需要根据数据特征选择相应的数据类型。字符串类型在使用时比较容易混淆,下表列出了openGauss中常见的字符串类型:
表 1 常用字符串类型
名称 |
描述 |
最大存储空间 |
---|---|---|
CHAR(n) |
定长字符串,n描述了存储的字节长度,如果输入的字符串字节格式小于n,那么后面会自动用空字符补齐至n个字节。 |
10MB |
CHARACTER(n) |
定长字符串,n描述了存储的字节长度,如果输入的字符串字节格式小于n,那么后面会自动用空字符补齐至n个字节。 |
10MB |
NCHAR(n) |
定长字符串,n描述了存储的字节长度,如果输入的字符串字节格式小于n,那么后面会自动用空字符补齐至n个字节。 |
10MB |
BPCHAR(n) |
定长字符串,n描述了存储的字节长度,如果输入的字符串字节格式小于n,那么后面会自动用空字符补齐至n个字节。 |
10MB |
VARCHAR(n) |
变长字符串,n描述了可以存储的最大字节长度。 |
10MB |
CHARACTER VARYING(n) |
变长字符串,n描述了可以存储的最大字节长度;此数据类型和VARCHAR(n)是同一数据类型的不同表达形式。 |
10MB |
VARCHAR2(n) |
变长字符串,n描述了可以存储的最大字节长度,此数据类型是为兼容Oracle类型新增的,行为和VARCHAR(n)一致。 |
10MB |
NVARCHAR2(n) |
变长字符串,n描述了可以存储的最大字节长度。 |
10MB |
TEXT |
不限长度(不超过1GB-8203字节)变长字符串。 |
1GB-8203字节 |
DEFAULT和NULL约束
【建议】如果能够从业务层面补全字段值,那么,就不建议使用DEFAULT约束,避免数据加载时产生不符合预期的结果。
局部聚簇
Partial Cluster Key(局部聚簇,简称PCK)是列存表的一种局部聚簇技术,在openGauss中,使用PCK可以通过min/max稀疏索引实现事实表快速过滤扫描。PCK的选取遵循以下原则:
唯一约束
【关注】行存表、列存表均支持唯一约束。
主键约束
【关注】行存表、列存表均支持主键约束。
外键约束
【关注】行存表支持外键约束,列存表不支持外键约束。
检查约束
【关注】行存表支持检查约束,而列存表不支持。
【建议】除非视图之间存在强依赖关系,否则不建议视图嵌套。
目前,openGauss相关的第三方工具都是通过JDBC进行连接的,此部分将介绍工具配置时的注意事项。
【关注】第三方工具通过JDBC连接openGauss时,JDBC向openGauss发起连接请求,会默认添加以下配置参数,详见JDBC代码ConnectionFactoryImpl类的实现。
复制代码params = {
{ "user", user },
{ "database", database },
{ "client_encoding", "UTF8" },
{ "DateStyle", "ISO" },
{ "extra_float_digits", "2" },
{ "TimeZone", createPostgresTimeZone() },
};
这些参数可能会导致JDBC客户端的行为与gsql客户端的行为不一致,例如,Date数据显示方式、浮点数精度表示、timezone显示。
如果实际期望和这些配置不符,建议在java连接设置代码中显式设定这些参数。
【建议】通过JDBC连接数据库时,应该保证下面三个时区设置一致:
openGauss数据库实例配置过程中时区。
说明: 时区设置相关的操作,请参考《安装指南》中“企业版安装>安装准备>准备软硬件安装环境> 同步系统时间”章节内容。
【关注】在应用程序中,如果需要使用fetchsize,必须关闭autocommit。开启autocommit,会令fetchsize配置失效。
【建议】在JDBC向openGauss申请连接的代码中,建议显式打开autocommit开关。如果基于性能或者其它方面考虑,需要关闭autocommit时,需要应用程序自己来保证事务的提交。例如,在指定的业务SQL执行完之后做显式提交,特别是客户端退出之前务必保证所有的事务已经提交。
【建议】推荐使用连接池限制应用程序的连接数。每执行一条SQL就连接一次数据库,是一种不好的SQL编写习惯。
【建议】在应用程序完成作业任务之后,应当及时断开和openGauss的连接,释放资源。建议在任务中设置session超时时间参数。
【建议】使用JDBC连接池,在将连接释放给连接池前,需要执行以下操作,重置会话环境。否则,可能会产生因为历史会话信息导致的对象冲突。
【建议】在不使用ETL工具,数据入库实时性要求又比较高的情况下,建议在开发应用程序时,使用openGauss JDBC驱动的copyManger接口进行微批导入。
【建议】在insert语句中显式给出插入的字段列表。例如:
复制代码INSERT INTO task(name,id,comment) VALUES ('task1','100','第100个任务');
【建议】在批量数据入库之后,或者数据增量达到一定阈值后,建议对表进行analyze操作,防止统计信息不准确而导致的执行计划劣化。
【建议】如果要清理表中的所有数据,建议使用truncate table方式,不要使用delete table方式。delete table方式删除性能差,且不会释放那些已经删除了的数据占用的磁盘空间。
【建议】使用下面时间相关的宏替换now函数来获取当前时间。因为now函数生成的执行计划无法下推,导致查询性能严重劣化。
表 1 时间相关的宏
宏名称 |
描述 |
示例 |
---|---|---|
CURRENT_DATE |
获取当前日期,不包含时分秒。 |
复制代码openGauss=# select CURRENT_DATE; date -———– 2018-02-02 (1 row) |
CURRENT_TIME |
获取当前时间,不包含年月日。 |
复制代码openGauss=# select CURRENT_TIME; timetz -——————- 00:39:34.633938+08 (1 row) |
CURRENT_TIMESTAMP(n) |
获取当前日期和时间,包含年月日时分秒。 说明:n表示存储的毫秒位数。 |
复制代码openGauss=# select CURRENT_TIMESTAMP(6); timestamptz -—————————— 2018-02-02 00:39:55.231689+08 (1 row) |
【建议】尽量避免标量子查询语句的出现。标量子查询是出现在select语句输出列表中的子查询,在下面例子中,下划线部分即为一个标量子查询语句:
复制代码SELECT id, (SELECT COUNT(*) FROM films f WHERE f.did = s.id) FROM staffs_p1 s;
标量子查询往往会导致查询性能急剧劣化,在应用开发过程中,应当根据业务逻辑,对标量子查询进行等价转换,将其写为表关联。
【建议】在where子句中,应当对过滤条件进行排序,把选择读较小(筛选出的记录数较少)的条件排在前面。
【建议】where子句中的过滤条件,尽量符合单边规则。即把字段名放在比较条件的一边,优化器在某些场景下会自动进行剪枝优化。形如col op expression,其中col为表的一个列,op为‘=’、‘>’的等比较操作符,expression为不含列名的表达式。例如,
复制代码SELECT id, from_image_id, from_person_id, from_video_id FROM face_data WHERE current_timestamp(6) - time < '1 days'::interval;
改写为:
复制代码SELECT id, from_image_id, from_person_id, from_video_id FROM face_data where time > current_timestamp(6) - '1 days'::interval;
【建议】尽量避免不必要的排序操作。排序需要耗费大量的内存及CPU,如果业务逻辑许可,可以组合使用ORDER BY和LIMIT,减小资源开销。openGauss默认按照ASC & NULL LAST进行排序。
【建议】使用ORDER BY子句进行排序时,显式指定排序方式(ASC/DESC),NULL的排序方式(NULL FIRST/NULL LAST)。
【建议】不要单独依赖limit子句返回特定顺序的结果集。如果部分特定结果集,可以将ORDER BY子句与Limit子句组合使用,必要时也可以使用OFFSET跳过特定结果。
【建议】在保障业务逻辑准确的情况下,建议尽量使用UNION ALL来代替UNION。
【建议】如果过滤条件只有OR表达式,可以将OR表达式转化为UNION ALL以提升性能。使用OR的SQL语句经常无法优化,导致执行速度慢。例如,将下面语句
复制代码SELECT * FROM scdc.pub_menu
WHERE (cdp= 300 AND inline=301) OR (cdp= 301 AND inline=302) OR (cdp= 302 ANDinline=301);
转换为:
复制代码SELECT * FROM scdc.pub_menu
WHERE (cdp= 300 AND inline=301)
union all
SELECT * FROM scdc.pub_menu
WHERE (cdp= 301 AND inline=302)
union all
SELECT * FROM tablename
WHERE (cdp= 302 AND inline=301)
【建议】当in(val1, val2, val3…)表达式中字段较多时,建议使用in (values (val1), (val2),(val3)…)语句进行替换。优化器会自动把in约束转换为非关联子查询,从而提升查询性能。
【建议】在关联字段不存在NULL值的情况下,使用(not) exist代替(not) in。例如,在下面查询语句中,当T1.C1列不存在NULL值时,可以先为T1.C1字段添加NOT NULL约束,再进行如下改写。
复制代码SELECT * FROM T1 WHERE T1.C1 NOT IN (SELECT T2.C2 FROM T2);
可以改写为:
复制代码 SELECT * FROM T1 WHERE NOT EXISTS (SELECT * FROM T1,T2 WHERE T1.C1=T2.C2);
说明:
如果不能保证T1.C1列的值为NOT NULL的情况下,就不能进行上述改写。
如果T1.C1为子查询的输出,要根据业务逻辑确认其输出是否为NOT NULL。