[TOC]
没有规矩,不成方圆。
PostgreSQL的功能非常强大,但是要把PostgreSQL用好,需要后端、运维、DBA的协力配合。
本文针对PostgreSQL数据库原理与特性,整理了一份开发/运维规约,希望可以减少大家在使用PostgreSQL数据库过程中遇到的困惑:你好我也好,大家都好。
本文第一版主要针对 PostgreSQL 9.4 - PostgreSQL 10 版本 ,当前最新版本针对 PostgreSQL 15/16 进行更新与调整。
计算机科学只存在两个难题:缓存失效和命名。
通用命名规则(Generic)
snake_case
风格。select pg_get_keywords();
获取保留关键字列表。$
,禁止使用中文,不要以 pg
开头。集群命名规则 (Cluster)
[a-z][a-z0-9-]*
。pg-<biz>-<tld>
。数据库类型 / 业务名称 / 业务线或环境biz
为最能代表业务特征的英文词语,应当仅由小写字母与数字组成,不得包含连字符 -
。biz
名应当为 <biz>delay
,例如 pg-testdelay
。biz
尾部添加数字:例如从 pg-user1
可以分支出 pg-user2
,pg-user3
等biz
命名中应当包含 shard
,并缀以分片号,例如 pg-testshard1
,pg-testshard2
,……<tld>
为顶层业务线,也可用于区分不同环境:例如 -tt
,-dev
,-uat
,-prod
等。无此需要可以省略。服务命名规则(Service)
pg-test-primary
,pg-test-replica
。primary
后缀命名,只读服务统一以 replica
后缀命名,这两个为必选服务。offline
后缀命名,直连主库/ETL写入以 default
后缀命名,为选配服务。standby
后缀命名,延迟从库服务以 delayed
后缀命名,少量核心库可提供此服务。实例命名规则(Instance)
-
拼接而成,例如: pg-test-1
,pg-test-2
。ins
标签,附加到该实例的所有数据上。数据库命名规则(Database)
<tld>_<biz>
的形式构建,<tld>
为顶层业务线,也可用于区分不同环境,不用可以省略。<biz>
为具体业务名称,例如 pg-test-tt
集群可以使用库名 tt_test
或 test
。这一点不强制,即允许创建不同于集群 <biz>
名称的其他数据库。<biz>
部分必须以shard
结尾,但不应当包含分片号,例如 pg-testshard1
,pg-testshard2
都用 testshard
即可。-
连接。例如:<biz>-chat-shard
,<biz>-payment
等,总共不超过三段。角色命名规范(Role/User)
dbsu
有且仅有一个:postgres
,用于流复制的用户命名为replicator
。dbuser_monitor
,用于日常管理的超级用户为:dbuser_dba
。dbuser_<biz>
作为用户名,例如 dbuser_test
。来自不同服务的访问应当使用独立的业务用户区分访问。dbp_<name>
,其中 name
为 LDAP 中的标准用户名。dbrole_readonly
,dbrole_readwrite
,dbrole_admin
,dbrole_offline
。模式命名规则(Schema)
<prefix>
作为模式名,尽可能简短,默认设置为search_path
首位元素。<prefix>
不得使用 public
,monitor
,不得与任何 PostgreSQL 扩展使用的模式名冲突,例如: timescaledb
,citus
,repack
,graphql
,net
,cron
,…… 不宜使用特殊名称:dba
,trash
。rel_<partition_total_num>_<partition_index>
。中间为总分片数,目前固定使用 8192 ,后缀是分片号,从0开始计数。如 rel_8192_0
,…… ,rel_8192_11
,等等。<prefix>
之外的模式名需要由研发解释其必要性。关系命名规则(Relation)
v_
作为命名前缀,物化视图使用mv_
作为命名前缀,临时表以tmp_
作为命名前缀。tbl_2023
,月级分区 tbl_202304
,天级分区 tbl_20230405
,小时级分区 tbl_2023040518
,默认分区以 _default
结尾。索引命名规则(Index)
_pkey
结尾,唯一索引以 _key
结尾,普通索引以 _idx
结尾,用于EXCLUDED
约束的索引以_excl
结尾。tbl_md5_title_idx
,tbl_ts_ge_2023_idx
,但不可超出长度限制。字段命名规则(Attribute)
oid
, xmin
, xmax
,cmin
,cmax
,ctid
。id
,或以id
作为后缀。created_time
,最后修改时间惯用名为 updated_time
is_
,has_
等作为前缀。extra
作为列名。枚举项命名 (Enum)
camelCase
,但也允许其他风格。函数命名规则(Function)
select
,insert
,delete
,update
,upsert
,create
,……。_by_ids
,_by_user_ids
的后缀在函数名中体现。BIGINT/INTEGER/SMALLINT
等整型进行函数签名重载,调用时可能产生歧义。$1
,$2
,…)。_
,例如_user_id
。注释规范(Comment)
COMMENT
),注释使用英文,言简意赅,一行为宜。Suum cuique
建表注意事项
EXCLUDE
约束是泛化的唯一约束,可以在低频更新场景下用于保证数据完整性。分区表注意事项
宽表注意事项
主键注意事项
PRIMARY KEY(a,b,...)
单列指定。UUID
与长度受限的文本类型,使用其他类型需要显式说明与评估。BIGINT
,谨慎使用 INTEGER
,不允许使用 SMALLINT
。GENERATED ALWAYS AS IDENTITY
生成唯一主键;SERIAL
,BIGSERIAL
仅当需要兼容 PG 10 以下版本时允许使用。UUID
类型作为主键,建议用 UUID v1/v7;谨慎使用 UUIDv4 作为主键,随机 UUID 的局部性较差且有碰撞概率。VARCHAR(64)
,使用更长的字符串时应当进行说明与评估。INSERT/UPDATE
时原则上禁止修改主键列的值,INSERT RETURNING
可用于返回自动生成的主键值。外键注意事项
SET NULL
, SET DEFAULT
, CASCADE
,慎用级联操作。空值/默认值注意事项
NOT NULL
约束。DEFAULT
默认值。数值类型注意事项
INTEGER
。容量拿不准的数值列使用BIGINT
。SMALLINT
,性能与存储提升甚小,但会有很多额外的问题。INTMAX
但没超过UINTMAX
的值需要升格存储。不要存储超过INT64MAX
的值到BIGINT
列中,会溢出为负数。REAL
表示4字节浮点数,FLOAT
表示8字节浮点数。浮点数仅可用于末尾精度无所谓的场景,例如地理坐标。切记不要对浮点数使用等值判断,零值除外。NUMERIC
,如果可行,请用 NUMERIC(p)
与NUMERIC(p,s)
设置有效数字位数以及小数部分的有效位数。例如摄氏气温(37.0
)可以用 NUMERIC(3,1)
类型来存储3位有效数字与1位小数。MONEY
。文本类型注意事项
char(n)
, varchar(n)
, text
。默认情况下,可以使用 text
类型 ,不限制字符串长度,但受字段最大长度1GB限制。varchar(n)
类型来设置一个最大字符串长度,这会引入极微小的额外检查开销,但能规避一些脏数据与极端情况。char(n)
,该类型为了与SQL标准兼容,存在不合直觉的行为表现(补齐空格与截断),且并没有存储和性能优势。时间类型注意事项
TIMESTAMPTZ
,不带时区的 TIMESTAMP
。TIMESTAMPTZ
,如果使用 TIMESTAMP
存储,必须使用0时区标准时。now() AT TIME ZONE 'UTC'
,不能直接截断时区 now()::TIMESTAMP
。2006-01-02 15:04:05
,避免DMY与MDY问题。Asia/Hong_Kong
+8 时区,因为上海时区缩写 CST
有歧义。枚举类型注意事项
ALTER TYPE <enum_name>
用于修改枚举。UUID类型注意事项
uuid-ossp
,pg_uuidv7
等,有此需求请在配置时指明。JSON类型注意事项
JSONB
类型与相关函数,而非文本版本的 JSON
。text
类型中不允许出现 \u0000
零字符,与 JSON 数值类型对应的 numeric
中不允许出现 NaN
与 infinity
。布尔值只接受小写的 true
与 false
字面值。null
对象和 SQL 标准中的空值 NULL
并非同一个概念。数组类型注意事项
pgvector
扩展提供的专用数据类型。GIS类型注意事项
触发器注意事项
updated_time
为当前时间戳,或者将表的增删改动作记录到另一张日志表中,或者维持两张表在业务上的一致性。存储过程/函数注意事项
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$;
使用合理的Locale选项
en_US.UTF8
,没有特殊理由不得更改。collate
规则必须为 C
,避免字符串索引问题。使用合理的字符编码与本地化配置
UTF8
字符编码,严格禁止使用其他任何字符编码。C
作为 LC_COLLATE
默认排序规则,有特殊需求必须在DDL/查询子句中显式指定来实现。LC_CTYPE
默认使用 en_US.UTF8
,一些扩展依赖字符集信息方可正常工作,如 pg_trgm
。索引相关注意事项
明确B树索引空值的顺序
NULLS FIRST
还是NULLS LAST
。DESC
排序的默认规则是NULLS FIRST
,即空值会出现在排序的最前面,通常这不是期望行为。CREATE INDEX ON tbl (id DESC NULLS LAST);
禁止在大字段上建立索引
varchar(n)
类型。充分利用函数索引
reverse
函数索引。充分利用部分索引
The limits of my language mean the limits of my world.
—Ludwig Wittgenstein
使用服务接入
读写分离
快慢分离
使用连接池
为查询语句配置主动超时
关注复制延迟
重试失败的事务
掉线重连
SELECT 1
作为心跳包查询,检测连接的有消息,并定期保活。在线服务应用代码禁止执行DDL
SELECT语句显式指定列名
SELECT *
,或在RETURNING
子句中使用*
。请使用具体的字段列表,不要返回用不到的字段。当表结构发生变动时(例如,新值列),使用列通配符的查询很可能会发生列数不匹配的错误。id
升级为 BIGINT
后,id
的列顺序会到最后一列。此问题只能在维护迁移时择机修复,研发应当克制调整列顺序的强迫症,并在 SELECT 语句中显式指定列的顺序禁止在线查询全表扫描
!=
, <>
的否定式操作符会导致全表扫描,必须避免。禁止在事务中长时间等待
IDEL IN Transaction
将被强制杀死。BEGIN
之后没有配对的ROLLBACK
或COMMIT
。使用 count 计数时的注意事项
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
。
进行比较,保证比较结果非空。注意序列号空缺
Serial
类型时,INSERT
,UPSERT
等操作都会消耗序列号,该消耗不会随事务失败而回滚。INTEGER
作为主键,且表存在频繁插入冲突时,需要关注整型溢出的问题。使用游标后必须及时关闭
重复查询使用准备语句
选择合适的事务隔离等级
r h 09判断结果存在性不要使用count
SELECT 1 FROM tbl WHERE xxx LIMIT 1
判断是否存满足条件的列,要比Count快。SELECT exists(SELECT * FROM tbl WHERE xxx LIMIT 1)
将存在性结果转换为布尔值。使用RETURNING子句一次性取回修改后的结果
RETURNING
子句可以在 INSERT
,UPDATE
,DELETE
语句后使用,有效减少数据库交互次数。使用UPSERT简化逻辑
UPSERT
替代。利用咨询锁应对热点并发。
优化IN操作符
EXISTS
子句代替IN
操作符,性能更佳。=ANY(ARRAY[1,2,3,4])
代替 IN (1,2,3,4)
,效果更佳。不建议使用左模糊搜索
WHERE col LIKE '%xxx'
无法充分利用B树索引,如有需要,可用reverse
表达式函数索引。使用数组代替临时表
=ANY(ARRAY[1,2,3])
要比临时表JOIN好。使用 Pigsty 搭建 PostgreSQL 集群与基础设施
pigsty.yml
配置文件通常包含了高度敏感的重要机密信息,应当使用 git 进行版本化管理,并严格控制访问权限。files/pki
内生成的 CA 私钥与其他证书应当妥善保管,定期将备份至安全区域存储归档,并严格控制访问权限。监控系统是必选项
根据需求合理规划集群架构
oltp
模板,分析类数据库使用 olap
模板,财务库使用 crit
模板,小微虚拟机(四核内)使用 tiny
模板。使用 Patroni 与 Etcd 配置集群高可用
etcd
使用专用虚拟机集群,3 ~ 5 个节点,严格打散分布在不同机柜上。使用 pgBackRest 与 MinIO 配置集群PITR
核心业务数据库配置注意事项
财务数据库配置注意事项
crit
模板,启用同步提交确保数据零丢失,视情况启用 Watchdog。使用合理的字符编码与本地化配置
UTF8
字符编码,严格禁止使用其他任何字符编码。C
作为 LC_COLLATE
默认排序规则,有特殊需求必须在DDL/查询子句中显式指定来实现。LC_CTYPE
默认使用 en_US.UTF8
,一些扩展依赖字符集信息方可正常工作,如 pg_trgm
。业务数据库管理注意事项
业务用户管理注意事项
pg_offline_query
的从库。扩展插件管理注意事项
yum/apt
安装对应大版本的扩展插件二进制软件包。shared_preload_libraries
,如果需要应当安排滚动重启。shared_preload_libraries
优先级顺序, citus
, timescaledb
,pgml
通常要放在最前面。pg_stat_statements
与 auto_explain
是必选插件,必须在所有集群中启用。dbsu
进行,在业务数据库中 CREATE EXTENSION
执行创建。数据库XID与年龄注意事项
VACUUM FREEZE
。数据库表与索引膨胀注意事项
pg_repack
在线处理表/索引膨胀问题。数据库重启注意事项
CHECKPOINT
两次,强制脏页刷盘,可加速重启过程。pg_ctl reload
重载配置确认配置文件正常可用。pg_ctl restart
或 patronictl 同时重启整个集群。kill -9
关闭任何数据库进程。复制延迟注意事项
新从库数据预热
pg_prewarm
将热数据加载至内存。数据库发布流程
数据工单格式
xx
库执行 yy
动作。数据库变更发布注意事项
dbuser_dba
远程执行,确保默认权限正常工作。SET ROLE dbrole_admin
后再执行发布,确保默认权限。psql
命令行工具,连接到集群主库执行,使用 \i
执行脚本或 \e
手工分批执行。删除表注意事项
DROP
应当首先重命名,冷却 1~3 天确认没有访问后再移除。trash
Schema 中,通过 ALTER TABLE SET SCHEMA
修改模式名。创建与删除索引注意事项
CREATE INDEX CONCURRENTLY
并发创建索引,使用 DROP INDEX CONCURRENTLY
并发移除索引。INVALID
的索引,修改索引后,使用 analyze
重新收集表上的统计数据。maintenance_work_mem
为更大的值加速索引创建。审慎地进行模式变更
VOLATILE
的函数,避免全表重写。ANALYZE
刷新统计信息。控制数据写入的批次规模
UPDATE
后,执行 VACUUM
回收死元组占用的空间。数据加载注意事项
COPY
加载数据,如有需要可以并行执行。autovacuum
,按需禁用触发器,并在加载完后再建立约束与索引。maintenance_work_mem
,增大max_wal_size
。vacuum verbose analyze table
。数据库迁移、大版本升级注意事项
pg_dump | psql
逻辑导出导入的方式停机升级。数据误删/误更新处理流程
pageinspect
与 pg_dirtyread
从坏表中抢救数据。数据腐坏处理流程
auto_vacuum
,定位错误根因,替换故障磁盘并补充新从库。pg_filedump
从表二进制文件中恢复数据。dd
生成仿造提交记录。数据库连接打满注意事项
pg_cancel_backend
或 pg_terminate_backend
。pg_terminate_backend
中止所有普通后端进程,从每秒一次(psql
\watch 1
)开始。并从监控系统确认连接情况,如果继续堆积,则不断提高杀连接查询的执行频次,例如每 0.1 秒一次,直到不再堆积为止。本文作者:Kevin@灼华
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!