18 深入专题:分区表设计
原生分区的裁剪优化、锁粒度、扩容缩容与性能权衡。
Citus 的 Subquery/CTE Push-Pull Execution 是什么?为什么需要它?
Citus 是 PG 的 sharding 插件(被微软收购后仍开源)。处理复杂 SQL 时它用推拉模型做跨节点数据交换:push 是 shard -> coordinator,pull 是 coordinator -> worker。当子查询/CTE 无法与主查询在同一次 fragment 内联执行时(例如子查询带 LIMIT,无法作为 fragment 的一部分),Citus 会递归规划:先独立执行子查询,把结果通过 PG COPY 协议汇总到 coordinator 作为 intermediate results(中间结果存成 FILE),再把这些中间结果推回各 worker,worker 通过 read_intermediate_result 函数(Function Scan)读取 FILE 参与外层 JOIN,最后把结果拉回 coordinator。这套 push-pull 机制让 Citus 能支持更多复杂 SQL 和混用多种 executor。
DB 吐槽大会:PG 不支持在线 split/merge 分区,业务怎么应对?
PG 早期不支持在线 split/merge 分区,拆分/合并要靠 detach+attach 手动操作,期间有锁和迁移成本。业务上通常在低峰期做,或用 pg_pathman 等工具。PG17 起原生支持 ALTER TABLE … MERGE PARTITION / SPLIT PARTITION 命令,解决了这个痛点。
DB 吐槽大会:PG 分区表不能自动创建/扩展分区,业务上如何规避?
PG 分区表不会在数据超出分区范围时自动创建新分区,这是被吐槽的痛点。业务上通常需要提前创建足够的未来分区(如按月提前建好一年的分区),或用定时任务/事件触发器在数据到达前自动建分区。牺牲的是需要额外维护脚本和提前规划分区边界,未来产品迭代方向是支持 interval/自动扩展分区(类似 pg_pathman 的 interval 分区)。
DB 吐槽大会:PG 分区表没有全局索引、不支持分区索引,带来什么问题?如何规避?
PG 分区表只有本地索引(每个分区各一棵),没有全局索引,也不支持把索引本身按分区组织(分区索引)。问题:本地索引无法跨分区保证唯一性/主键(非分区键字段无法做全局唯一约束),不带分区键的点查要探测多个分区。规避:业务层保证唯一性或加路由表;用 partial index + 多棵树模拟;未来方向是全局索引(leaf 页需 keyvalue -> tableoid + ctid 以知道记录在哪个子表)。
DuckDB 如何用 parquet 文件实现分区表/Delta Lake 能力?
DuckDB 支持 parquet 外部存储直接读写、pushdown/projection 下推、多目录/通配符,非常适合用 parquet 文件实现 Delta Lake。用多个 parquet 文件模拟分区,导出到分区文件目录(如按分区字段的路径组织),parquet_scan 可配置 HIVE_PARTITIONING 和 FILENAME 参数增加虚拟字段(分区字段名、文件路径)。分区条件可下推,收敛需要扫描的 parquet 文件——输入一级/二级分区条件时只扫对应文件,未含分区条件则扫全部。注意分区字段名不能与表字段重名。
PG hash 分区表如何扩容、缩容?hash 算法和 MODULUS/REMAINDER 有什么关系?
hash 分区通过 MODULUS(模数)和 REMAINDER(余数)定义分区。扩容/缩容的关键是对齐 MODULUS 与 REMAINDER:例如 1024 个分区合并到 512 个,或 512 扩到 1024,只要是倍数关系即可避免数据全部重新计算。hash 分区的 hash 算法是 hash_any/hashint4 等(代码在 partbounds.c/partition.h),注意:对值做 hashint4 取余后的结果与子分区 REMAINDER 没有直接关系,不能用简单取模来预测数据落在哪个分区——要用专门的 hash 分片计算函数。一个分区表甚至可以有不同的 MODULUS 子分区。
PG 内置 sharding(基于 FDW)的演进到了什么程度?还缺什么?
PG 社区在基于 FDW 接口演进内置 sharding。目前 FDW 已能实现 DML 和 query,并支持下推 sort/filter/agg/join,也支持 FDW 分区表,已具备 sharding 雏形。但尚需改进:一是 FDW 分区串行运行,多分区查询性能不线性(而 plproxy/citus/pg-xc/greenplum 已支持异步并行);二是缺少 replica table(join with replica table)优化;三是尚缺 shard 语法与 GSM(Global Snapshot Manager 全局快照管理器)。PG11 已可通过 hint 让多分区并行,未来会支持更好。
PG12 之前操作单个分区会锁所有分区,PG12 之后锁粒度有什么变化?
PG12 之前对分区表执行 DML 时,会对所有分区加锁(从 pg_locks 可见大量 p0~p255 的 RowExclusiveLock),即使只写一个分区。PG12 优化为只对目标分区和主表加锁,操作单一分区时 LOCK 仅单个分区以及主分区。这大幅降低了锁竞争,是高并发下分区表写入性能提升的关键因素之一。
PG12 分区表性能提升百倍是怎么测出来的?关键数字是多少?
对比单表与 1024 个分区的分区表(各 1 亿记录)的查询和 upsert:单表性能基本持平(查询 116 万 qps 左右、upsert 33 万 qps 左右),但 1024 分区表在 PG11 下查询仅 1163 qps、upsert 2885 qps,PG12 beta1 提升到查询 545602 qps(469 倍)、upsert 246627 qps(85 倍)。根因是 PG12 之前优化器对分区表会为所有分区创建 RangeTblEntry 和 RelOptInfo 元数据,分区多时 overhead 巨大。
PG12 对分区表外键(PK 作为其他表的 FK)做了什么支持?
PG12 支持分区表的主键(PK)作为其他表的外键(FK),补齐了分区表在引用完整性上的能力缺口。
PG12 的 plan-time partition pruning 解决了原生分区表什么性能问题?带来多大提升?
PG12 之前,优化器无论 SQL 只操作单个分区还是多个分区,都会为所有分区创建 RangeTblEntry 与 RelOptInfo 结构并加锁,分区一多开销巨大。PG12 把这个过程推迟到处理完 query、识别出 restriction quals 并做完 partition pruning 之后:对 plan time 就能确定不会访问的分区,直接不创建其数据结构、不打开其 relation。带来的收益是内存降低、plan 更快、TPS 提升——256 个分区的写入测试从 PG11 的 11348 tps 涨到 PG12 的 267447 tps,提升约 23.57 倍;1024 分区下查询提升 469 倍、upsert 提升 85 倍。实际可支持的分区数上限也从约 100 个提升到几千个。
PG12 里 psql 用什么快捷命令列出分区表?
PG12 起 psql 新增快捷命令 \dP 用来列出分区表(此前没有专门的快捷命令)。
PG13 pgbench 的 tpcb 内置模型对分区表做了什么支持?
PG13 的 pgbench 内置 tpcb 模型支持把 pgbench_account 表建成分区表,可通过选项指定 range 或 hash 分区,省去了手工创建分区表的繁琐步骤,方便快速测试分区带来的开销(如 hash 分区对简单 select 的开销)。
PG14 在分区裁剪和 UPDATE/DELETE 上又做了哪些优化?
PG14 有两个相关优化:一是 ExecInitModifyTable 分区裁剪精细化,不再 touch 不必要的分区;二是 Rework planning and execution of UPDATE and DELETE——减少传导不必要的列 value、避免为每个分区生成 subplan,降低分区表 DML 的执行开销。
PG15 postgres_fdw 的异步增强对基于 FDW 的 sharding 有什么意义?
PG15 postgres_fdw 支持更多异步操作,增强基于 FDW 的 sharding 能力:包括 DML 异步写入、分区表、union all、union 等。此前 PG14 已引入 FDW 异步执行接口和 postgres_fdw 异步 append,这次进一步扩展到更多操作,让基于 FDW 的 sharding 写性能和并行能力更好。
PG15 的「未裁剪分区 bitmapset」优化解决了什么?
PG15 在 RelOptInfo 中记录未裁剪分区的 Bitmapset(Track a Bitmapset of non-pruned partitions),避免重复计算裁剪结果,降低 plan time,进一步提升分区表的规划性能。
PG16 的「分区缓存」优化解决什么问题?适用什么场景?
PG16 的分区查找优化(分区缓存)针对同一会话中连续写入同一分区的场景:缓存上次命中的分区,减少每次二分法查找分区的开销,提升分区表批量写入性能。不适用的场景是写入在分区之间频繁跳转的情况。
PG17 的 ALTER TABLE … MERGE|SPLIT PARTITION 是什么?
PG17 实现了 ALTER TABLE … MERGE PARTITION 和 SPLIT PARTITION 命令,原生支持分区的合并与拆分,结束了此前只能靠 detach+attach 手工操作、不支持在线 split/merge 的历史。
PG17 还做了哪些分区表相关改进?
PG17 的分区表改进包括:支持修改分区表的 access method(如切换存储引擎/表访问方法);减少 partitionwise join 的内存消耗;以及实现 ALTER TABLE … MERGE|SPLIT PARTITION 命令。
PG18 为什么移除了对分区表 unlogged 的支持?作者怎么看?
PG18 的 patch 移除分区表 unlogged 支持:原因是此前分区表被创建为 unlogged 时,分区并没有继承该属性,导致分区表实质上仍是 logged 的。社区选择直接不支持分区表 unlogged(设置时报错),作者吐槽这是「不解决问题、解决提问题的人」——分区表不配 unlogged,如果是国产数据库这么干估计要被骂死。
PG19 的 COPY 分区表 TO 相比之前有什么改进?
PG19 支持直接 COPY 分区表 TO 导出。此前分区表只能用 COPY (SELECT … FROM 分区表) TO 实现,多了一次 query 处理,性能更差。注意 RLS(行安全策略)在 copy to 中依旧有效,看不到的数据无法导出。
PGcat 中间件相比 pgbouncer/pgpool-II/citus 的定位是什么?
pgbouncer 效率高但是单进程;pgpool-II 支持读写分离但本身效率一般;citus 支持 sharding 但需装插件、对非微软云的 RDS 不友好。pgcat 是 Rust 写的 PG 中间件,同时支持 sharding+连接池+读写分离+负载均衡+故障转移:应用层(OSI 7)负载均衡,理解 wire protocol,支持 RR/随机/最少活跃连接策略,可把 SELECT 路由到副本、其他 query 路由到主库;维护实时健康主机列表,副本健康检查失败自动切负载(不对 primary 做故障转移);支持事务/会话模式连接池;用原生 PG 解析器提取分片键做 sharding 路由,跨分片查询在内存组装结果。
ShardingSphere 的三种模式(Sharding-JDBC/Proxy/Sidecar)如何选?适合什么业务?
ShardingSphere 是与 MySQL sharding 中间件类似的 PG 分库分表中间件,适合分片彻底、数据库逻辑极其简单的业务。三种模式:Sharding-JDBC(任意数据库、仅 Java、损耗低、无中心化、连接消耗高)、Sharding-Proxy(MySQL/PG、任意语言、损耗略高、有静态入口)、Sharding-Sidecar(MySQL/PG、任意语言、损耗低、无中心化、连接消耗高)。使用注意:自定义函数内部若有写操作会路由到只读库导致报错;sql.show 建议关闭否则打印大量日志影响性能。
Yugabyte 在 SaaS 多租户场景如何用 RLS + GEO 分区实现隔离与就近存储?
Yugabyte 是兼容 PG 的分布式全球数据库,支持 multi-region,可用 geo tablespace 实现数据就近存储(如 A 用户数据放欧洲表空间)。SaaS 多租户隔离分两种:逻辑隔离用 RLS(行安全策略,也叫 VPD),物理隔离用不同表。通常按数据量、用户等级选择——VIP 客户常用物理隔离。Yugabyte 演示了 pgbench + RLS + geo tablespace + 分区表组合,做到新租户不增加额外资源(共享 database/连接池/schema)又通过 RLS 保证安全隔离。
pg_dump 对分区表和继承表做了什么过滤支持?
PG16 的 pg_dump 支持对分区表和继承表的条件过滤项,可以在导出时按条件过滤子表(分区/继承子表),便于选择性备份。
pg_pathman 分区表如何无损转换为 PG12 原生分区表?
PG12 原生分区性能已与 pg_pathman 持平,因此可以把 pg_pathman 分区表转换为原生分区表,且无需迁移数据,只改继承关系:pg_pathman 的 hash 分区转原生 list 分区(因其底层用 get_hash_part_idx(hashint4(id), n) 定义,对应原生 list),range 分区直接转换。做法是 NO INHERIT 解除继承、再 attach 到原生分区表。注意作为 list 分区的表达式必须是 immutable 函数,需修改函数属性。转换后原生写入速度与 pg_pathman 一样。
pg_rewrite 插件如何在线把普通表转换为分区表?有什么限制?
pg_rewrite(cybertec 开源)解决从非分区表变更为分区表的长时间锁问题:用 logical replication 从非分区表增量复制数据到新的分区表,同步完成后只需短暂的排他锁切换表名(有参数控制锁超时,如 100ms 重试 3 次)。限制:非分区表必须有 PK;分区表的 check/not null/FK/default 等约束要与非分区表保持一致;serial 字段要处理妥当。
pgbench 的 client_id 变量有什么用?能解决什么问题?
client_id 是 pgbench 中唯一标识客户端会话的变量(从 0 开始)。用它可以模拟数据隔离的更新操作,避免多个连接更新到相同记录导致的锁冲突;或用 mod 让每个 client 更新不同的 ID,确保不同会话行级锁互不重叠。实测不用 client_id 时锁冲突严重,qps 从 25 万降到 18 万。client_id 也期望将来能作为动态表名后缀,实现不同线程操作不同表(类似分区表的压测隔离),但 pgbench 暂未支持 identify 字段中放变量。
pgdog 相比 pgcat/pgbouncer 的 sharding 能力有什么特点?
pgdog 是支持「分片/负载均衡/连接池」于一体的 PG 代理:理解 PG wire protocol,代理多个副本和主库并按 RR/随机/最少活跃连接均衡,能把 SELECT 发副本、其他 query 发主库;维护实时健康主机列表,副本健康检查失败自动切负载(对 primary 不做切换只重试);支持事务/会话模式连接池(事务模式允许数十万客户端只用几个后端连接);sharding 用原生 PG 解析器理解 query、提取分片键确定路由,跨分片查询在内存组装转换结果。相比 pgbouncer(单进程)、pgpool-II(效率一般)、citus(需插件、云服务不友好),pgdog 无需插件且功能更全。
plproxy 是什么?它的定位和特点是什么?
plproxy 是基于函数接口的 PG sharding 插件,用于分库分表,非常灵活、性能损耗很低,早在 200x 年就被 Skype 广泛使用。使用它需要写存储过程接口,应用侵入最大,但性能最好。plproxy 2.9 版本支持 PG 11 和 12。
为什么全文检索 bm25 得分排序不建议使用分区表?
pg_textsearch(timescale)把计算 BM25 得分所需信息存在索引里,但对分区表每个分区维护自己的(分区本地)统计信息,没有全局索引,导致跨分区按 bm25 得分排序无意义:每个分区的 IDF 值依赖分区本地的文档总数和词频统计,相同查询词在不同分区产生不同得分,跨分区得分不可直接比较。单分区查询得分准确,跨分区查询则不可比。
为什么分区表分区过多会导致性能下降?用分区表的正确动机是什么?
用分区表的根本动机是规避「单表太大」的通用危害:1) 垃圾回收/冻结是单表单进程粒度,表大导致回收慢、膨胀、xid 可能耗尽;2) 单表逻辑备份/恢复无法并行、耗时变长;3) 单表只能放一个表空间/文件系统,无法用多块盘并行吞吐;4) 逻辑复制初始全量同步无法并行、中断后要重来;5) 表可能超出寻址边界(CTID 4 字节 blockid,8K block 最大 32TB);6) 按时间清理历史数据时没有分区只能 delete,产生大量 WAL 和膨胀。但分区过多本身也有害:relcache 内存暴增、执行计划变慢、可能 rte 溢出、全表查询要大量 open/close fd。所以分区要在「规避单表危害」和「分区过多开销」之间权衡。
为什么应用端直接计算 hash 分区能绕过 catalog 开销提速 20 倍?
PG hash 分区用确定性哈希把 tuple 分布到各分区。查询父表时 PG 必须做 catalog 查找才能把 query 路由到正确分区,高吞吐 OLTP(尤其多级分区)下这是明显开销——多级分区要遍历更深的 catalog 结构。有人用 Ruby 库嫁接 PG 内置哈希分区计算代码(hashfn.c/hashfunc.c),在应用端直接算出目标分区、直接访问分区表,绕过 catalog 查找,分区查找性能提升 20 倍。前提是应用能自主识别确定分区。
为什么要把原生分区表转回普通表?怎么做?
业务预估过多时,开发可能把所有表都建成了分区表,实际并不需要。分区表的代价:高并发下引入优化器损耗;分区多会让会话 relcache 内存增加,长连接+高并发+未用 huge page 可能 OOM。转换方法是解除继承关系、把分区数据并入新普通表(或切换表名)。需注意 serial 字段的 sequence 与表挂钩问题:删除旧表会导致序列被删、新表默认值被清,要先解除 sequence 的 owner 依赖。
为什么要经常手工对分区表的主表执行 ANALYZE?
分区表是「入口分区 -> 分支分区 -> 叶子分区」的分层结构,数据只存在叶子分区,autovacuum 触发垃圾回收和自动统计信息收集时只收集叶子分区的统计信息,不修改非叶子(入口/分支)分区的统计信息。但实际使用大量查询走的是入口(主表),如果主表统计信息不及时更新,会导致很差的执行计划。所以需要经常手工对主表执行 ANALYZE 更新统计信息。
什么是「非对称分区表智能 JOIN」?它想解决什么?
某些场景下需要把分区数据 append 后再 JOIN,必须改写 SQL 才能做到基于每个分区 JOIN 后合并结果。非对称分区表智能 JOIN(PG15 期待的特性)旨在自动优化这种 SQL 改写:当两个分区表 JOIN 字段类型一致、分区在 JOIN 字段上、分区类型一致(枚举/LIST/范围/HASH)、分区个数一致时,优化器自动选择 partitionwise join,让子分区各自 JOIN 子分区,类似 MPP 的 co-located join。
分区表 ORDER BY 分区键时,为什么 PG12 可以避免 merge sort?
当分区表按分区字段 ORDER BY 且各分区本身有序时,各分区返回的结果天然有序,直接 append 拼接即可,不需要 merge append(merge sort)的额外归并排序开销。PG12 支持分区表 order by 分区键时使用 append(ordered scan partition),PG15 进一步把这个能力扩展到更多 order by key 场景——例如 list 分区里一个分区含多个 value 时,只要保证分区内有序也能走 append scan,避免 merge append 的 mergesort 计算。
分区表全局索引与分布式数据库全局二级索引分别解决什么问题?核心取舍是什么?
分区表全局索引:单库分区表中,本地索引无法直接跨分区保证唯一性或点查效率——如果订单表按 created_at 分区,而业务按 order_no/user_id 查一条记录,本地索引要探测多个分区。全局索引维护一套跨分区索引键空间,用非分区键直接定位行,解决三类问题:跨分区点查(非分区键查少量记录不扫多分区)、跨分区唯一性(order_no/email/id_card 整表唯一)、访问路径与生命周期解耦。分布式数据库全局二级索引是同一类问题的分布式版本,只是「分区」变成可切分/复制/迁移的 range。两者实现不同但取舍相同:把读路径的多分区扫描,换成写路径/事务路径/维护路径的全局成本——每次写入要写全局索引,分区 DROP/DETACH/TRUNCATE 要处理对应条目,唯一性检查跨分区延迟扩大,全局索引本身可能成为热点(单调递增键、低基数键、集中写入键)。
分区表跨分区排序 + limit 怎么用 merge append 减少扫描量?
分区表跨分区按非分区字段排序再 limit 输出时,用归并排序(merge append sort)减少扫描量:各分区分别有序返回,再做归并。多个分段 SQL 按某字段排序 limit 分页返回,也可用 union all + merge append sort 优化。核心是让每个分段(分区)内返回有序结果,MergeAppend 归并取 top-N,避免全量排序。
分区过多会导致什么错误?为什么分区不是越多越好?
分区过多(超过 65535 个)会导致 range table entry 溢出报错 too many range table entries。分区过多的通用问题:高并发时需要更多内存存 relcache、执行计划变慢、内存暴增;可能溢出;全表查询性能变差(大量 open/close fd)。作者建议不要为几万条记录就建一个分区,几亿记录也没必要分区;应只在遇到瓶颈时考虑分区,例如垃圾回收、freeze、创建索引、表级逻辑备份。经验值:频繁更新的表每分区 1 亿以内较好,插入量大更新少查询多的表可考虑 10 亿单分区。
如何把 PolarDB PostgreSQL 的大表平滑转换为分区表?
思路:先建好目标分区表(如按月分区),用迁移工具做全量+增量同步(如 NineData 社区版,基于逻辑复制建 replication slot 同步),到业务低谷时切换表名,有约束关系的再迁移约束,把对业务影响降到最低。需要 wal_level=logical 支持增量,pg_hba 允许迁移工具连接(含流复制协议)。
如何根据分区键的值计算它落在哪个 hash 分区?
PG 的 hash 分区分片计算代码在 partbounds.c(partition.h 定义相关结构),通过 hash 函数对分区键值做哈希再映射到分片。可以通过 SQL 接口函数把原始值转换为 hash 分片 ID(从 0 开始计数),int4/text/uuid 等类型分别实现。验证方法是:用该函数算出的分片与 EXPLAIN 执行计划中显示的目标分区对照,返回 0 条差异即说明正确。注意 hashint4 取余结果与 REMAINDER 无直接关系,不能直接拿 mod 结果当分区号。
如何用 detach/attach 对分区表做拆分和合并?PG 支持什么样的分区布局?
PG 支持通过 ALTER TABLE … DETACH PARTITION 解绑分区、ATTACH PARTITION 绑定分区来实现拆分与合并。拆分例子(hash):先 detach 掉某个分区,再创建一个二级分区表,把数据迁移进去,最后把二级分区表 attach 回原分区上,这样原来一个分区就被一个二级分区表替代。PG 支持非常灵活的分区布局:支持任意层级、每个分区深度可不一致(非平衡分区表),甚至可以把某个分区改成别的分区方法(如 hash 改 list)。这特别适合数据分布不均的场景——例如某 id 落在同一分区但数据量巨大,可对它再做二级分区。
推荐系统「已读过滤」导致大量 CPU/IO 浪费时,用 hash 分片+partial index 怎么优化?
场景:10 亿 user、10 亿 video,按 weight 排序选 vids 并过滤已读(用 HLL 记录 vid hash 判断已读)。已读列表越大,按 weight 倒排查出的记录大量是已读的,浪费大量时间在 HLL 运算上。优化思路:把表按随机索引分区(如 20 个分区),每次只查询一个分区,查询范围缩小到 1/20,该分区内的已读量也变成 1/20,offset 量降低 20 倍,性能明显提升(实测 147.7ms 降到 12.1ms)。代价是与业务略有偏差(只查到部分记录),但从整体拉平看,用户请求次数足够多时随机能覆盖所有记录。