前言
写这篇文章的缘由是,我在浏览 Greenplum 最佳实践的时候,看到这么一句话
不要使用默认分区。默认分区总是会被扫描,更重要的是很多情况下会导致溢出而造成性能不佳。
我立马眉头一皱,难道 GP 的优化器这么搓?于是立马去试验了一下,发现并不是这样 (GP7),看样子是 7 版本以前对于 postgres-planner 的最佳实践 (ORCA 会裁剪)。回到 PG,在 11 版本中便支持了默认分区,由于 PG 仍尚未原生支持 Interval 分区,因此只能提前创建好足够多的分区,否则一旦插入了一条不满足既定分区规则的行便会报错:ERROR: no partition of relation xxx found for row
。这个时候,默认分区的好处就出来了,默认分区就相当于一个垃圾桶,来者不拒,凡是不满足规则的行都会存入到该默认分区中,这种行为看似短期内利好了应用,但无疑不是长久之计,危害种种。
何如
举个栗子,该表现在有一个默认分区
1 | postgres=# \d+ ptab01 |
该默认分区同样也有约束,很明了 — 前面两个分区上的约束取反,因此不满足分区规则的行便”满足”了这个默认分区的规则,即所谓的来者不拒。
1 | Partition of: ptab01 DEFAULT |
短期内,应用舒爽了,但是时间一长,随着默认分区里的数据越来越多,危害便接踵而至,首先如前文所述:
- 对于 GP7 以前的版本,按照三水的原话“永远不要使用默认分区,never,因为总是会扫描”
- 其次,如果现在想新增一个三月的子分区会怎样?(数据现在已经在默认分区里了)
是的,对于后者,会直接报错:
1 | postgres=# create table ptab01_202303 partition of ptab01 for values from ('2023-03-01') to ('2023-04-01'); |
那数据库是怎么知道这个默认分区里面有没有冲突的数据呢?有没有一种快捷的方式呢?很遗憾,需要挨个扫一遍,因为需要精确扫描确保没有数据冲突 (倒是不会导致 rewrite),除了顺序扫描我也暂时想不到还有啥简便的方式了。因此对于这种情况,只能手动处理冲突的数据,
1 | postgres=# delete from ptab01_default ; |
不难想象,如果堆了大几百 GB 的默认分区,后面要进行维护,比如新增分区,那无疑是一件苦差事,这不又回到了以前的老大难问题:数据量大 → 为了好维护进行分区 → 没有维护好,又全部进入了默认分区。同理,如果你想将一个表挂载为默认分区,也需要全表扫一遍。
那如果真遇到了这种情况,该怎么办?其实 postgres-howto 里面已经给出了许多解决方案:
把默认分区 detach 掉,处理完冲突数据后再 attach 回来,12 以后 attach 只需要 4 级锁,在 14 中也支持了 detach concurrently:In postgresql 14, DETACH PARTITION partitioned_table_1 CONCURRENTLY,SHARE UPDATE EXCLUSIVE lock on the parent table
默认的 create table xxx partition of 会全程获取 8 级锁,因此最佳实践是先使用 including like 的语法,然后再 attach,当然这种方法针对的是 12 以后的版本,在 12 以前,Attach 在父表和被连接分区上都要加上 8 级锁 🔐。
同理,想要新增分区给表添加约束时 (方法同样适用于添加 FK、PK 等),如果直接执行类似
alter table t add constraint c_id_is_positive check (id > 0);
在此期间,会扫描所有数据,全程 8 级锁,因此最佳实践应该是alter table t add constraint c_id_is_positive check (id > 0) not valid;
,这样的话只需要简短的 8 级锁 (搭配 lock_timeout 和 retry),最后再alter table t validate constraint c_id_is_positive;
,这个校验步骤就只需要 4 级锁了,不阻塞读写。值得注意的是,当NOT VALID
约束添加之后,新的数据写入会立即进行检查 (而旧数据尚未验证,可能会违反约束)。
简而言之,遵循锁的获取原则:
- 够用即可:使用满足条件的锁中最弱的锁模式
- 越快越好:如果可能,可以用 (长时间的弱锁+短时间的强锁) 替换长时间的强锁
- 递增获取:遵循 2PL 原则申请锁;越晚使用激进锁策略越好;在真正需要时再获取。
- 相同顺序:获取锁尽量以一致的顺序获取,从而减小死锁的几率
后记
默认分区,作为最佳实践,建议能不用就不用,如果真用了,需要定期巡检,确保默认分区里的数据量不要过大。默认分区可以看做是一个过渡阶段,期待原生 PG 可以早日支持 Interval 分区!
参考
https://github.com/Vonng/pg/blob/master/app/sql-lock.md
https://github.com/yydzero/yydzero.github.io/blob/master/articles/gpdb-best-practice.md