PostgreSQL开发规约
0x00背景
没有规矩,不成方圆。
PostgreSQL的功能非常强大,但是要把PostgreSQL用好,需要后端、运维、DBA的协力配合。
本文针对PostgreSQL数据库原理与特性,整理了一份开发规范,希望可以减少大家在使用PostgreSQL数据库过程中遇到的困惑。 你好我也好,大家都好。
0x01 命名规范
无名,万物之始,有名,万物之母。
【强制】 通用命名规则
- 本规则适用于所有对象名,包括:库名、表名、表名、列名、函数名、视图名、序列号名、别名等。
- 对象名务必只使用小写字母,下划线,数字,但首字母必须为小写字母,常规表禁止以
_
打头。 - 对象名长度不超过63个字符,命名统一采用
snake_case
。 - 禁止使用SQL保留字,使用
select pg_get_keywords();
获取保留关键字列表。 - 禁止出现美元符号,禁止使用中文,不要以
pg
开头。 - 提高用词品味,做到信达雅;不要使用拼音,不要使用生僻冷词,不要使用小众缩写。
【强制】 库命名规则
- 库名最好与应用或服务保持一致,必须为具有高区分度的英文单词。
- 命名必须以
<biz>-
开头,<biz>
为具体业务线名称,如果是分片库必须以-shard
结尾。 - 多个部分使用
-
连接。例如:<biz>-chat-shard
,<biz>-payment
等,总共不超过三段。
【强制】 角色命名规范
- 数据库
su
有且仅有一个:postgres
,用于流复制的用户命名为replication
。 - 生产用户命名使用
<biz>-
作为前缀,具体功能作为后缀。 - 所有数据库默认有三个基础角色:
<biz>-read
,<biz>-write
,<biz>-usage
,分别拥有所有表的只读,只写,函数的执行权限。 - 生产用户,ETL用户,个人用户通过继承相应的基础角色获取权限。
- 更为精细的权限控制使用独立的角色与用户,依业务而异。
【强制】 模式命名规则
- 业务统一使用
<*>
作为模式名,<*>
为业务定义的名称,必须设置为search_path
首位元素。 dba
,monitor
,trash
为保留模式名。- 分片模式命名规则采用:
rel_<partition_total_num>_<partition_index>
。 - 无特殊理由不应在其他模式中创建对象。
【推荐】 关系命名规则
- 关系命名以表意清晰为第一要义,不要使用含混的缩写,也不应过分冗长,遵循通用命名规则。
- 表名应当使用复数名词,与历史惯例保持一致,但应尽量避免带有不规则复数形式的单词。
- 视图以
v_
作为命名前缀,物化视图使用mv_
作为命名前缀,临时表以tmp_
作为命名前缀。 - 继承或分区表应当以父表表名作为前缀,并以子表特性(规则,分片范围等)作为后缀。
【推荐】 索引命名规则
- 创建索引时如有条件应当指定索引名称,并与PostgreSQL默认命名规则保持一致,避免重复执行时建立重复索引。
- 用于主键的索引以
_pkey
结尾,唯一索引以_key
结尾,用于EXCLUDED
约束的索引以_excl
结尾,普通索引以_idx
结尾。
【推荐】 函数命名规则
- 以
select
,insert
,delete
,update
,upsert
打头,表示动作类型。 - 重要参数可以通过
_by_ids
,_by_user_ids
的后缀在函数名中体现。 - 避免函数重载,同名函数尽量只保留一个。
- 禁止通过
BIGINT/INTEGER/SMALLINT
等整型进行重载,调用时可能产生歧义。
【推荐】 字段命名规则
- 不得使用系统列保留字段名:
oid
,xmin
,xmax
,cmin
,cmax
,ctid
等。 - 主键列通常命名为
id
,或以id
作为后缀。 - 创建时间通常命名为
created_time
,修改时间通常命名为updated_time
- 布尔型字段建议使用
is_
,has_
等作为前缀。 - 其余各字段名需与已有表命名惯例保持一致。
【推荐】 变量命名规则
- 存储过程与函数中的变量使用命名参数,而非位置参数。
- 如果参数名与对象名出现冲突,在参数后添加
_
,例如user_id_
。
【推荐】 注释规范
- 尽量为对象提供注释(
COMMENT
),注释使用英文,言简意赅,一行为宜。 - 对象的模式或内容语义发生变更时,务必一并更新注释,与实际情况保持同步。
0x02 设计规范
Suum cuique
【强制】 字符编码必须为UTF8
- 禁止使用其他任何字符编码。
【强制】 容量规划
- 单表记录过亿,或超过10GB的量级,可以考虑开始进行分表。
- 单表容量超过1T,单库容量超过2T。需要考虑分片。
【强制】 不要滥用存储过程
- 存储过程适用于封装事务,减少并发冲突,减少网络往返,减少返回数据量,执行少量自定义逻辑。
- 存储过程不适合进行复杂计算,不适合进行平凡/频繁的类型转换与包装。
【强制】 存储计算分离
- 移除数据库中不必要的计算密集型逻辑,例如在数据库中使用SQL进行WGS84到其他坐标系的换算。
- 例外:与数据获取、筛选密切关联的计算逻辑允许在数据库中进行,如PostGIS中的几何关系判断。
【强制】 主键与身份列
- 每个表都必须有身份列,原则上必须有主键,最低要求为拥有非空唯一约束。
- 身份列用于唯一标识表中的任一元组,逻辑复制与诸多三方工具有赖于此。
【强制】 外键
- 不建议使用外键,建议在应用层解决。使用外键时,引用必须设置相应的动作:
SET NULL
,SET DEFAULT
,CASCADE
,慎用级联操作。
【强制】 慎用宽表
- 字段数目超过15个的表视作宽表,宽表应当考虑进行纵向拆分,通过相同的主键与主表相互引用。
- 因为MVCC机制,宽表的写放大现象比较明显,尽量减少对宽表的频繁更新。
【强制】 配置合适的默认值
- 有默认值的列必须添加
DEFAULT
子句指定默认值。 - 可以在默认值中使用函数,动态生成默认值(例如主键发号器)。
【强制】 合理应对空值
- 字段语义上没有零值与空值区分的,不允许空值存在,须为列配置
NOT NULL
约束。
【强制】 唯一约束通过数据库强制。
- 唯一约束须由数据库保证,任何唯一列须有唯一约束。
EXCLUDE
约束是泛化的唯一约束,可以在低频更新场景下用于保证数据完整性。
【强制】 注意整数溢出风险
- 注意SQL标准不提供无符号整型,超过
INTMAX
但没超过UINTMAX
的值需要升格存储。 - 不要存储超过
INT64MAX
的值到BIGINT
列中,会溢出为负数。
【强制】 统一时区
- 使用
TIMESTAMP
存储时间,采用utc
时区。 - 统一使用ISO-8601格式输入输出时间类型:
2006-01-02 15:04:05
,避免DMY与MDY问题。 - 使用
TIMESTAMPTZ
时,采用GMT/UTC时间,0时区标准时。
【强制】 及时清理过时函数
- 不再使用的,被替换的函数应当及时下线,避免与未来的函数发生冲突。
【推荐】 主键类型
- 主键通常使用整型,建议使用
BIGINT
,允许使用不超过64字节的字符串。 - 主键允许使用
Serial
自动生成,建议使用Default next_id()
发号器函数。
【推荐】 选择合适的类型
- 能使用专有类型的,不使用字符串。(数值,枚举,网络地址,货币,JSON,UUID等)
- 使用正确的数据类型,能显著提高数据存储,查询,索引,计算的效率,并提高可维护性。
【推荐】 使用枚举类型
- 较稳定的,取值空间较小(十几个内)的字段应当使用枚举类型,不要使用整型与字符串表示。
- 使用枚举类型有性能、存储、可维护性上的优势。
【推荐】 选择合适的文本类型
- PostgreSQL的文本类型包括
char(n)
,varchar(n)
,text
。 - 通常建议使用
varchar
或text
,带有(n)
修饰符的类型会检查字符串长度,会导致微小的额外开销,对字符串长度有限制时应当使用varchar(n)
,避免插入过长的脏数据。 - 避免使用
char(n)
,为了与SQL标准兼容,该类型存在不合直觉的行为表现(补齐空格与截断),且并没有存储和性能优势。
【推荐】 选择合适的数值类型
- 常规数值字段使用
INTEGER
。主键、容量拿不准的数值列使用BIGINT
。 - 无特殊理由不要用
SMALLINT
,性能与存储提升很小,会有很多额外的问题。 REAL
表示4字节浮点数,FLOAT
表示8字节浮点数- 浮点数仅可用于末尾精度无所谓的场景,例如地理坐标,不要对浮点数使用等值判断。
- 精确数值类型使用
NUMERIC
,注意精度和小数位数设置。 - 货币数值类型使用
MONEY
。
【推荐】 使用统一的函数创建语法
- 签名单独占用一行(函数名与参数),返回值单启一行,语言为第一个标签。
- 一定要标注函数易变性等级:
IMMUTABLE
,STABLE
,VOLATILE
。 - 添加确定的属性标签,如:
RETURNS NULL ON NULL INPUT
,PARALLEL SAFE
,ROWS 1
,注意版本兼容性。
CREATE OR REPLACE FUNCTION
nspname.myfunc(arg1_ TEXT, arg2_ INTEGER)
RETURNS VOID
LANGUAGE SQL
STABLE
PARALLEL SAFE
ROWS 1
RETURNS NULL ON NULL INPUT
AS $function$
SELECT 1;
$function$;
【推荐】 针对可演化性而设计
- 在设计表时,应当充分考虑未来的扩展需求,可以在建表时适当添加1~3个保留字段。
- 对于多变的非关键字段可以使用JSON类型。
【推荐】 选择合理的规范化等级
- 允许适当降低规范化等级,减少多表连接以提高性能。
【推荐】 使用新版本
- 新版本有无成本的性能提升,稳定性提升,有更多新功能。
- 充分利用新特性,降低设计复杂度。
【推荐】 慎用触发器
- 触发器会提高系统的复杂度与维护成本,不鼓励使用。
0x03 索引规范
Wer Ordnung hält, ist nur zu faul zum Suchen.
【强制】 在线查询必须有配套索引
- 所有在线查询必须针对其访问模式设计相应索引,除极个别小表外不允许全表扫描。
- 索引有代价,不允许创建不使用的索引。
【强制】 禁止在大字段上建立索引
- 被索引字段大小无法超过2KB(1/3的页容量),原则上禁止超过64个字符。
- 如有大字段索引需求,可以考虑对大字段取哈希,并建立函数索引。或使用其他类型的索引(GIN)。
【强制】 明确空值排序规则
- 如在可空列上有排序需求,需要在查询与索引中明确指定
NULLS FIRST
还是NULLS LAST
。 - 注意,
DESC
排序的默认规则是NULLS FIRST
,即空值会出现在排序的最前面,通常这不是期望行为。 - 索引的排序条件必须与查询匹配,如:
create index on tbl (id desc nulls last);
【强制】 利用GiST索引应对近邻查询问题
- 传统B树索引无法提供对KNN问题的良好支持,应当使用GiST索引。
【推荐】 利用函数索引
- 任何可以由同一行其他字段推断得出的冗余字段,可以使用函数索引替代。
- 对于经常使用表达式作为查询条件的语句,可以使用表达式或函数索引加速查询。
- 典型场景:建立大字段上的哈希函数索引,为需要左模糊查询的文本列建立reverse函数索引。
【推荐】 利用部分索引
- 查询中查询条件固定的部分,可以使用部分索引,减小索引大小并提升查询效率。
- 查询中某待索引字段若只有有限几种取值,也可以建立几个相应的部分索引。
【推荐】 利用范围索引
- 对于值与堆表的存储顺序线性相关的数据,如果通常的查询为范围查询,建议使用BRIN索引。
- 最典型场景如仅追加写入的时序数据,BRIN索引更为高效。
【推荐】 关注联合索引的区分度
- 区分度高的列放在前面
0x04 查询规范
The limits of my language mean the limits of my world.
—Ludwig Wittgenstein
【强制】 读写分离
- 原则上写请求走主库,读请求走从库。
- 例外:需要读己之写的一致性保证,且检测到显著的复制延迟。
【强制】 快慢分离
- 生产中1毫秒以内的查询称为快查询,生产中超过1秒的查询称为慢查询。
- 慢查询必须走离线从库,必须设置相应的超时。
- 生产中的在线普通查询执行时长,原则上应当控制在1ms内。
- 生产中的在线普通查询执行时长,超过10ms需修改技术方案,优化达标后再上线。
- 在线查询应当配置10ms数量级或更快的超时,避免堆积造成雪崩。
- Master与Slave角色不允许大批量拉取数据,数仓ETL程序应当从Offline从库拉取数据
【强制】 主动超时
- 为所有的语句配置主动超时,超时后主动取消请求,避免雪崩。
- 周期性执行的语句,必须配置小于执行周期的超时。
【强制】 关注复制延迟
- 应用必须意识到主从之间的同步延迟,并妥善处理好复制延迟超出合理范围的情况
- 平时在0.1ms的延迟,在极端情况下可能达到十几分钟甚至小时量级。应用可以选择从主库读取,稍后再度,或报错。
【强制】 使用连接池
- 应用必须通过连接池访问数据库,连接6432端口的pgbouncer而不是5432的postgres。
- 注意使用连接池与直连数据库的区别,一些功能可能无法使用(比如Notify/Listen),也可能存在连接污染的问题。
【强制】 禁止修改连接状态
- 使用公共连接池时禁止修改连接状态,包括修改连接参数,修改搜索路径,更换角色,更换数据库。
- 万不得已修改后必须彻底销毁连接,将状态变更后的连接放回连接池会导致污染扩散。
【强制】 重试失败的事务
- 查询可能因为并发争用,管理员命令等原因被杀死,应用需要意识到这一点并在必要时重试。
- 应用在数据库大量报错时可以触发断路器熔断,避免雪崩。但要注意区分错误的类型与性质。
【强制】 掉线重连
- 连接可能因为各种原因被中止,应用必须有掉线重连机制。
- 可以使用
SELECT 1
作为心跳包查询,检测连接的有消息,并定期保活。
【强制】 在线服务应用代码禁止执行DDL
- 不要在应用代码里搞大新闻。
【强制】 显式指定列名
- 避免使用
SELECT *
,或在RETURNING
子句中使用*
。请使用具体的字段列表,不要返回用不到的字段。当表结构发生变动时(例如,新值列),使用列通配符的查询很可能会发生列数不匹配的错误。 - 例外:当存储过程返回具体的表行类型时,允许使用通配符。
【强制】 禁止在线查询全表扫描
- 例外情况:常量极小表,极低频操作,表/返回结果集很小(百条记录/百KB内)。
- 在首层过滤条件上使用诸如
!=
,<>
的否定式操作符会导致全表扫描,必须避免。
【强制】 禁止在事务中长时间等待
- 开启事务后必须尽快提交或回滚,超过10分钟的
IDEL IN Transaction
将被强制杀死。 - 应用应当开启AutoCommit,避免
BEGIN
之后没有配对的ROLLBACK
或COMMIT
。 - 尽量使用标准库提供的事务基础设施,不到万不得已不要手动控制事务。
【强制】 使用游标后必须及时关闭
【强制】 科学计数
count(*)
是统计行数的标准语法,与空值无关。count(col)
统计的是col
列中的非空记录数。该列中的NULL值不会被计入。count(distinct col)
对col
列除重计数,同样忽视空值,即只统计非空不同值的个数。count((col1, col2))
对多列计数,即使待计数的列全为空也会被计数,(NULL,NULL)
有效。a(distinct (col1, col2))
对多列除重计数,即使待计数列全为空也会被计数,(NULL,NULL)
有效。
【强制】 注意聚合函数的空值问题
- 除了
count
之外的所有聚合函数都会忽略空值输入,因此当输入值全部为空时,结果是NULL
。但count(col)
在这种情况下会返回0,是一个例外。 - 如果聚集函数返回空并不是期望的结果,使用
coalesce
来设置缺省值。
【强制】谨慎处理空值
- 明确区分零值与空值,空值使用
IS NULL
进行等值判断,零值使用常规的=
运算符进行等值判断。 - 空值作为函数输入参数时应当带有类型修饰符,否则对于有重载的函数将无法识别使用何者。
- 注意空值比较逻辑:任何涉及到空值比较运算结果都是
unknown
,需要注意unknown
参与布尔运算的逻辑:and
:TRUE or UNKNOWN
会因为逻辑短路返回TRUE
。or
:FALSE and UNKNOWN
会因为逻辑短路返回FALSE
- 其他情况只要运算对象出现
UNKNOWN
,结果都是UNKNOWN
- 空值与任何值的逻辑判断,其结果都为空值,例如
NULL=NULL
返回结果是NULL
而不是TRUE/FALSE
。 - 涉及空值与非空值的等值比较,请使用``IS DISTINCT FROM
- 空值与聚合函数:聚合函数当输入值全部为NULL时,返回结果为NULL。
【强制】 注意序列号空缺
- 当使用
Serial
类型时,INSERT
,UPSERT
等操作都会消耗序列号,该消耗不会随事务失败而回滚。 - 当使用整型作为主键,且表存在频繁插入冲突时,需要关注整型溢出的问题。
【推荐】 重复查询使用准备语句
- 重复的查询应当使用准备语句(Prepared Statement),消除数据库硬解析的CPU开销。
- 准备语句会修改连接状态,请注意连接池对于准备语句的影响。
【推荐】 选择合适的事务隔离等级
- 默认隔离等级为读已提交,适合大多数简单读写事务,普通事务选择满足需求的最低隔离等级。
- 需要事务级一致性快照的写事务,请使用可重复读隔离等级。
- 对正确性有严格要求的写入事务请使用可序列化隔离等级。
- 在RR与SR隔离等级出现并发冲突时,应当视错误类型进行积极的重试。
【推荐】 判断结果存在性不要使用count
- 使用
SELECT 1 FROM tbl WHERE xxx LIMIT 1
判断是否存满足条件的列,要比Count快。 - 可以使用
select exists(select * FROM app.sjqq where xxx limit 1)
将存在性结果转换为布尔值。
【推荐】 使用RETURNING子句
- 如果用户需要在插入数据和,删除数据前,或者修改数据后马上拿到插入或被删除或修改后的数据,建议使用
RETURNING
子句,减少数据库交互次数。
【推荐】 使用UPSERT简化逻辑
- 当业务出现插入-失败-更新的操作序列时,考虑使用
UPSERT
替代。
【推荐】 利用咨询锁应对热点并发。
- 针对单行记录的极高频并发写入(秒杀),应当使用咨询锁对记录ID进行锁定。
- 如果能在应用层次解决高并发争用,就不要放在数据库层面进行。
【推荐】优化IN操作符
- 使用
EXISTS
子句代替IN
操作符,效果更佳。 - 使用
=ANY(ARRAY[1,2,3,4])
代替IN (1,2,3,4)
,效果更佳。
【推荐】 不建议使用左模糊搜索
- 左模糊搜索
WHERE col LIKE '%xxx'
无法充分利用B树索引,如有需要,可用reverse
表达式函数索引。
【推荐】 使用数组代替临时表
- 考虑使用数组替代临时表,例如在获取一系列ID的对应记录时。
=ANY(ARRAY[1,2,3])
要比临时表JOIN好。
0x05 发布规范
【强制】 发布形式
- 目前以邮件形式提交发布,发送邮件至dba@p1.com 归档并安排提交。
- 标题清晰:xx项目需在xx库执行xx动作。
- 目标明确:每个步骤需要在哪些实例上执行哪些操作,结果如何校验。
- 回滚方案:任何变更都需要提供回滚方案,新建也需要提供清理脚本。
【强制】发布评估
- 线上数据库发布需要经过研发自测,主管审核,(可选QA审核),DBA审核几个评估阶段。
- 自测阶段应当确保变更在开发、预发环境执行正确无误。
- 如果是新建表,应当给出记录数量级,数据日增量预估值,读写量级预估。
- 如果是新建函数,应当给出压测报告,至少需要给出平均执行时间。
- 如果是模式迁移,必须梳理清楚所有上下游依赖。
- Team Leader需要对变更进行评估与审核,对变更内容负责。
- DBA对发布的形式与影响进行评估与审核。
【强制】 发布窗口
- 19:00 后不允许数据库发布,紧急发布请TL做特殊说明,抄送CTO。
- 16:00点后确认的需求将顺延至第二天执行。(以TL确认时间为准)
0x06 管理规范
【强制】 关注备份
- 每日全量备份,段文件持续归档
【强制】 关注年龄
- 关注数据库与表的年龄,避免事物ID回卷。
【强制】 关注老化与膨胀
- 关注表与索引的膨胀率,避免性能劣化。
【强制】 关注复制延迟
- 监控复制延迟,使用复制槽时更必须十分留意。
【强制】 遵循最小权限原则
【强制】并发地创建与删除索引
- 对于生产表,必须使用
CREATE INDEX CONCURRENTLY
并发创建索引。
【强制】 新从库数据预热
- 使用
pg_prewarm
,或逐渐接入流量。
【强制】 审慎地进行模式变更
- 添加新列时必须使用不带默认值的语法,避免全表重写
- 变更类型时,必要时应当重建所有依赖该类型的函数。
【推荐】 切分大批量操作
- 大批量写入操作应当切分为小批量进行,避免一次产生大量WAL。
【推荐】 加速数据加载
- 关闭
autovacuum
,使用COPY
加载数据。 - 事后建立约束与索引。
- 调大
maintenance_work_mem
,增大max_wal_size
。 - 完成后执行
vacuum verbose analyze table
。