17 深入专题:存储引擎与垃圾回收

17 深入专题:存储引擎与垃圾回收

解析 Vacuum、Freeze、TOAST 与表膨胀等存储层核心机制。

64 位 XID 改造能解决和不能解决什么问题?

能解决:freeze 风暴根源(不再有半圆快耗尽的焦虑)、autovacuum 必须成功的强约束(XID 不循环,vacuum 慢一点也不致命)、长事务导致停库的极端情况、从库延迟与 freeze 的耦合。不能解决:表膨胀(仍需 vacuum/autovacuum 清理死元组)、长事务本身的回滚代价、长事务持有的锁。即 64 位 XID 消除了 wraparound 风险,但没有改变 MVCC 死元组与膨胀的基本面。

Arrow 是什么?为什么说它是面向内存和进程 0 拷贝共享数据的列存设计?

Arrow 是一种列式内存数据格式(In-Memory Columnar Format),设计目标是让不同进程/系统之间共享数据时无需序列化/反序列化:数据以连续内存的列缓冲组织,配合零拷贝 IPC,跨进程、跨语言(C++/Python/Java 等)可直接读写同一块内存。它解决了传统行式数据在进程间传递需要序列化、以及列式计算中内存布局不紧凑的问题,是数据分析和向量化执行的底座。

BYPASS_THRESHOLD_PAGES 优化(PG14)如何减少不必要的 index vacuum?

PG14 引入 BYPASS_THRESHOLD_PAGES 优化:当 heap 页中被 LP_DEAD 覆盖的 page 较少(未达到阈值)时,跳过 index vacuum 阶段,避免每次 vacuum 都要扫描一遍所有索引。因为如果 dead tuple 只集中在少量页,索引里的 dead entries 也少,直接跳过索引清理更划算,可显著降低 vacuum 成本。

FSM(Free Space Map)和 VM(Visibility Map)在垃圾回收中分别解决什么问题?

FSM 记录每个 page 近似有多少 free space,目的是快速找到足够容纳新 tuple 的 page;它不记精确字节,而是每 heap page 一个字节的近似值,组织成树,根节点快速判断有没有足够大的空闲页。VACUUM 扫描时调用 RecordPageWithFreeSpace(),并周期性用 FreeSpaceMapVacuumRange() 向上层传播空闲信息。VM 记录每个 heap page 两个 bit:all-visible 和 all-frozen,用于让 index-only scan 跳过 heap fetch、让 vacuum 跳过已全可见的页面;它是保守结构,bit 设上时必为真,没设不代表一定为假。简言之 FSM 解决’新版本写到哪里’,VM 解决’哪些页面可以少看’。

HOT vacuum 收缩链路对 DML where CTID=ctid 安全吗?为什么?

不安全。HOT 更新会把旧版本通过 t_ctid 链指向新版本,vacuum 收缩(清理 HOT 链)后,旧版本的 t_ctid 可能已被回收或指向不可预期的位置。如果用 WHERE ctid = 某个物理行号来做 DELETE/UPDATE,由于 ctid 是物理位置且会被 vacuum/HOT 收缩改变,定位到的行可能已不是预期的那行(甚至已复用给别的行)。因此业务 SQL 不应依赖 ctid 做定位,ctid 只能用于诊断或临时定位,且用完即弃。

Heikki page epoch 方案为什么用 PageLSN 推断 epoch 而不显式存储?有什么 corner case?

LSN 是单调递增的物理时间戳。XID 走过 2^31 边界(进入新 half-epoch)时,在 pg_control 里记录该边界的 LSN;MVCC 解读 tuple 时读 page 的 PageLSN,查 half-epoch 边界表确定 PageLSN 落在哪个 epoch,再用 tuple.xmin + epoch 拼出 64 位 XID。这样 epoch 不用显式存,page 格式 0 字节开销。corner case:完全不更新、不删除的 tuple,其 PageLSN 永远不变,指向的 epoch 可能已走远,此时仍需显式冻结(autovacuum_freeze_max_age = 2*10^9 兜底),但只扫有未冻结 tuple 的 page。由于一个 tuple 在被更新前最多老 2^31,一个 page 上的 tuple XID 最多跨 2 个 half-epoch,pg_control 只需存最近 2 个边界。

LSM-Tree 的 size-tiered 和 leveled 两种 compaction 策略各自的取舍是什么?

size-tiered:每层 SST 数量有固定阈值(如 4 个),某层达到阈值就把该层所有 SST 合并成一个更大的放上层。优点是简单、SST 数量少、定位快;缺点是空间放大严重(大 SST 合并瞬间磁盘占用可能翻倍,重复 key 多时放大因子可到数倍)。leveled:L0 之外每层 SST 的 key 区间互不相交(一个 run),层间按 10 倍增长,compaction 时只选该层若干文件与下层有交集的合并。优点是空间放大小;缺点是写放大更严重(一对多合并,同等条件下写放大可达 size-tiered 的两倍以上,极端数十倍)。

LSM-Tree 的三放大(读放大、写放大、空间放大)分别指什么?

读放大:读取数据时实际读取的数据量大于真正需要的数据量,LSM 里需要从 MemTable 开始逐层向下查多个 SSTable 才能找到 key。写放大:写入时实际写入量大于真实数据量,compaction 会让同一个 key 在向高层沉淀过程中被反复重写,有多少层就写多少次。空间放大:数据占用的磁盘空间比真实大小多,因为 LSM 的增删改都是 append,旧版本要等 compaction 执行到该 key 才会被清理,一个 key 可能同时存在多个 value(删除标记也是特殊 value)。

Lance 列存格式的定位是什么?相比 Parquet 有什么优势?

Lance 是面向 AI/多模态工作负载的现代列式数据格式,定位为’超越 Parquet’。它在列存基础上针对机器学习训练、向量检索等场景做了优化,支持高效的随机访问、向量化读取和版本化(支持追加、更新),并兼容 Arrow 生态。相比 Parquet 的批量读定位,Lance 更适合 AI 训练数据管道、向量数据等需要频繁小批量随机读和可变长数据的场景。

ORC 列存格式与 Parquet 的主要定位差异是什么?

ORC(Optimized Row Columnar)也是一种列式存储格式,最初为 Hive 设计,按 stripe(行组)、column 组织,支持列级压缩、编码、轻量级索引(min/max、布隆过滤器)和内嵌统计。与 Parquet 相比定位相近,都是列存分析格式,但 ORC 在 stripe 内携带更丰富的索引/统计(如布隆过滤器),查询时数据跳过更细;Parquet 生态更通用、跨引擎支持更广。两者都用于数据仓库/数据湖的列存。

OrioleDB 是什么?它的 undo 和 copy-on-write checkpoint 机制有什么特点?

OrioleDB 是 PostgreSQL 的一个 table access method(存储引擎),基于 B+ 树组织数据,采用 undo(回滚段)而非 heap+MVCC 死元组方式处理并发,更新时旧版本进 undo segment,避免死元组累积和 vacuum 压力。它支持 copy-on-write checkpoint,checkpoint 时不阻塞写,通过写时复制快照实现一致恢复,降低 checkpoint 对业务的冲击。

PG 32 位 XID 困境的实质是什么?Heikki 2013 page epoch 方案的核心思想是什么?

32 位 XID 困境的实质不是'4 字节存事务号’那么简单,而是 4 字节事务号 + 环形比较 + 半数冻结:XID 是 uint32,最多 43 亿,用完循环,PG 把空间切成两半区分过去/未来,一旦走过半圆边界老 XID 就变’未来’,必须靠 freeze 全表扫来防止。Heikki 2013 page epoch 方案的核心是:在 page header 存一个 epoch(或用 PageLSN 推断 epoch),tuple header 的 t_xmin 仍是 32 位,但 MVCC 解读时真实 XID = (page.epoch « 32) | tuple.xmin。这样不破坏 page 格式、不增加 tuple 头大小、freeze 从’全表扫’降级为’自然变更’。

PG Data Block 中 LSN 的作用是什么?

每个数据页的 page header 里存有 PageLSN,记录最后一次修改该页的 WAL 记录的 LSN。它的作用:1) 崩溃恢复时,用页的 LSN 与要重放的 WAL 记录 LSN 比较,判断该页是否已包含该条 WAL 的修改,从而决定是否重放(保证幂等、避免重复应用);2) 判断脏页刷盘与 WAL 刷盘的前后关系,实施 WAL-before-data。它是崩溃恢复正确性和幂等性的关键。

PG unlogged table 转 logged 为什么会写大量 REDO/WAL?

unlogged table 正常写入时不写 WAL(数据不保证崩溃恢复),转换回 logged 时需要把表中已有数据全部补充写 WAL(否则崩溃后无法恢复这些数据)。这个转换过程会为整张表的数据生成大量 REDO/WAL,表越大写的 WAL 越多,可能造成 WAL 尖峰。这是 unlogged 表的固有代价,转换时应评估 WAL 量。

PG17 用 TidStore 替代旧的 dead tuple 存储结构提升了 vacuum 什么?为什么说 PG 单表不建议超过 8.9 亿条记录?

PG17 引入 TidStore 数据结构存储 dead tupleid,替代旧的数组结构,提升 vacuum 在记录和查找 dead TID 时的效率。之所以说 PG 单表不建议超过 8.9 亿条记录,是因为按默认 autovacuum 参数(1GB 内存记录 dead tupleid,tupleid 6 字节约 1.7 亿条)配合 20% 触发阈值,1GB 内存最多覆盖约 8.9 亿行表的垃圾;超过这个量级,单次 vacuum 无法一次性装下所有 dead TID,需要多次扫描索引,维护成本上升。

PG19 对哈希索引的改动为什么是’改写 VACUUM 的 I/O 逻辑’?

哈希索引的 bulk delete(vacuum 清理索引中的死项)此前用随机访问方式读取索引页,IO 效率低。PG19 把 streaming read(流式/预取式顺序读)用于 hash index 的 bulk delete,把散乱的随机读改成可预取的顺序读,显著提升哈希索引在 vacuum 阶段的清理效率。

PG19 的 Autovacuum 并行化如何突破大表维护瓶颈?

传统 autovacuum 单 worker 处理单表,大表 vacuum 是串行的,成为维护瓶颈。PG19 引入 autovacuum 并行化,让单张表的多索引清理(index vacuum 阶段)可以并行执行,配合 parallel vacuum 机制,由多个 worker 分工清理同一张表的不同索引,大幅缩短大表 vacuum 耗时。并行度受 max_parallel_maintenance_workers、autovacuum_max_parallel_workers 等参数约束。

PG19 的 Autovacuum 智能优先级是怎么让关键表先被处理的?

它重构了 autovacuum 的调度评分:不再简单按单一阈值,而是综合 XID/MXID 年龄、死元组数量、插入量、统计信息陈旧度等维度打分,并让接近 wraparound 危险区的表(XID 年龄高)获得更高优先级,确保防 wraparound 这类数据安全维护优先于普通空间回收。配套的 pg_stat_autovacuum_scores 视图让这个评分对外可观测。

PG19 的 CHECKPOINT 精细化控制支持哪些 MODE?FLUSH_UNLOGGED 是什么?

PG19 支持 CHECKPOINT 语句的精细化控制,可指定 MODE 为 FAST(快速,尽可能快完成)、SPREAD(散布,控制刷脏速率降低对业务 IO 冲击)等;FLUSH_UNLOGGED 控制是否把 unlogged table 的数据也 flush 出去。这让 DBA 能根据场景选择立即 checkpoint 还是平滑 checkpoint,避免一次 checkpoint 引发的 IO 尖峰。

PG19 的 REPACK 意味着什么?为什么说 vacuum full 将成为历史?

REPACK 是 PG19 引入的在线表重写/收缩方案,目标是替代需要强锁和额外磁盘空间的 VACUUM FULL。传统 VACUUM FULL 用 ACCESS EXCLUSIVE 锁整表重写,对在线业务影响大;REPACK 以接近在线的方式完成表压缩、回收空洞,让’收缩表’不再是一个需要维护窗口的重操作。

PG19 的 vacuum 优化补丁为什么最多能少产生 50% WAL?

它优化了 vacuum 在扫描/清理过程中对 page 的写操作,减少不必要的 full-page image 和增量 WAL 记录的产生(例如避免对未被实际修改的页写 WAL、合并小改动、减少 page 头的更新)。vacuum 扫大表时即使只改一点点也要写 WAL,这个补丁让 vacuum 只对真正有变化的部分写 WAL,从而最多可少产生约 50% 的 WAL。

PG19 的并行 VACUUM 把什么写进了日志?

PG19 让并行 VACUUM 把’计划启动几个 worker、实际真正拉起几个’写进日志。此前并行 vacuum 只报告整体进度,无法知道并行度是否按预期生效(可能因为 max_parallel_maintenance_workers、autovacuum 资源或索引数不足导致实际并行度低于计划)。现在日志能显示 plan 与实际启动的 worker 数,便于诊断并行 vacuum 是否真正并行。

PG19 硬件加速的校验和计算是什么?

PG19 为 data page 校验和(checksum)计算引入硬件加速:在 x86 上用 AVX2 指令、在 ARM 上用 CRC32C 指令实现校验和计算,替代原来的软件标量实现,大幅降低启用 data checksums 时的 CPU 开销。这让开启校验和的性能代价变小,鼓励更多场景开启数据校验和以检测静默损坏。

Parquet 列存格式的核心特点是什么?

Parquet 是面向分析的列存文件格式,按列组织数据并支持列级压缩、编码(如字典编码、RLE、bit-packing)和嵌套结构。它以 row group 和 column chunk 为单位组织,元数据里保存每列的统计信息(min/max、null count),查询时可据此跳过无关数据(谓词下推),适合 OLAP 大表扫描。Parquet 是数据湖生态的事实标准,pg_duckdb、pg_mooncake 等插件让 PostgreSQL 能直接查询 Parquet 文件。

PostgreSQL 12 的 blackhole(黑洞)存储引擎是什么?有什么用途?

blackhole 是 PG12 基于 table access method API 实现的一个’黑洞’存储引擎:写入的数据被直接丢弃(类似 MySQL 的 BLACKHOLE),读取恒为空。用途包括:测试复制链路和 SQL 解析开销(数据不落盘、写入极快)、作为某些中间层/审计场景的 sink,以及演示 AM 框架的可扩展性。

PostgreSQL 三种心跳(keepalive)指标是什么?分别用于什么场景?

三种心跳指标:1) 时间戳(时间心跳),用于判断实例/进程是否还活着、延迟;2) redo/WAL 位点(LSN 心跳),用于主从复制延迟监控、判断备库追上主库的位置;3) 事务号(XID 心跳),用于监控事务消耗速率、预测 freeze/wraparound 风险。三者分别对应’时间维度’‘数据持久化进度’‘事务生命周期’,是 DBA 监控数据库健康的三类基础探针。

PostgreSQL 列存相比行存有哪些优势?为什么列存没有 1666 列限制?

列存按列组织存储,同一列的数据连续存放。优势:1) 列存没有行存 1666 列的限制(行存受 tuple 最大宽度约束);2) 大量记录扫描时只读取需要的列,节约 IO 和资源,OLAP 聚合/过滤场景收益大;3) 便于按列做向量化计算和压缩。行存则适合按行整取、频繁更新的 OLTP 场景。二者结合即行列混合存储(HTAP)。

PostgreSQL 列存索引/混合索引的思路是什么?适合什么负载?

混合存储(行存+列存)与混合索引的思路是:OLTP 热数据用行存、OLAP 分析数据用列存,并在列存上建列存索引或物化/向量化结构加速扫描。适合 HTAP 混合负载——既需要高频单行事务读写,又需要对大量历史数据做聚合分析的场景,用一套数据库同时承载两种负载,避免 ETL 搬运。

PostgreSQL 有哪些膨胀点?哪些垃圾(dead tuple)和 WAL 文件不能被回收复用?

膨胀点分为:全局 catalog、库级 catalog、普通表、WAL 文件。dead tuple 若被老快照、长事务、prepared transaction、复制槽 xmin/catalog_xmin 或逻辑复制保留,就无法被 vacuum 回收;WAL 文件若被复制槽、归档、逻辑解码等下游未消费,就无法被复用/回收。因此膨胀的本质是’有人还留着更老的边界’,排查膨胀要同时看表和 WAL 两个层面的阻塞者。

VACUUM 的 BUFFER_USAGE_LIMIT 选项(PG16)和 vacuum_buffer_usage_limit 参数有什么用?

它限制 vacuum 使用的共享缓冲(shared buffer)量,通过控制 vacuum 环状缓冲(buffer ring)的大小来减少 vacuum 造成的 WAL flush 和其他页面被挤出缓存的影响。设小一点可以降低 vacuum 对 shared buffer 和 wal flush 的压力,提升整体速度、避免 vacuum 与业务争缓存。PG17 调大了 vacuum_buffer_usage_limit 的默认值(从 256KB 提到 2MB),进一步减少 vacuum 造成的 wal flush、提升 vacuum 速度。

VACUUM 的 PROCESS_TOAST 开关(PG14)和 MAIN/TOAST 选项(PG16)分别控制什么?

PG14 引入 vacuum PROCESS_TOAST 开关,控制 vacuum 是否同时处理关联的 TOAST 表(大字段外部存储)。PG16 进一步支持在 VACUUM 语句中指定仅处理 MAIN 或仅处理 TOAST,例如 VACUUM (PROCESS_MAIN FALSE) 或只处理 TOAST,让 DBA 能按需定向清理主表或 TOAST 表,减少不必要的维护开销。

VACUUM/ANALYZE 的 SKIP_LOCKED(PG12)解决什么问题?

PG12 引入 VACUUM (SKIP_LOCKED) 和 ANALYZE (SKIP_LOCKED),让 vacuum/analyze 在遇到被其他会话锁定的表时跳过该表而不是等待或报错。这解决了批量维护时个别表被锁导致整个维护任务卡住的问题,尤其适合在业务高峰期或大量表上跑 vacuumdb 时避免长时间锁等待。

Vortex 列存格式是什么定位?

Vortex 是一种面向分析/数据湖的列存新范式,同样基于 Arrow 生态,强调高效的向量化读取和压缩,针对现代 CPU(SIMD 向量化)做了数据布局优化,目标是在保持压缩率的同时提升解压与扫描吞吐。它与 Lance、Parquet 同属数据湖列存阵营,各有侧重。

WAL Sender 关闭超时(PG19)解决复制环境下的什么问题?

PG19 为 WAL Sender 引入关闭超时机制:当主库要关闭或切换时,WAL sender 进程若长时间无法正常结束(例如备库网络中断导致 send 阻塞),会按超时强制终止,避免主库关闭/切换被卡住的 WAL sender 拖住。这让复制环境下的优雅关闭/切换更可靠。

WAL 的 insert/write/flush 三个 LSN 水位分别代表什么?

三个 LSN 对应 WAL 的不同持久化进度:insert LSN 是已写入 WAL buffer 的位点;write LSN 是已由 walwriter 调用 write() 写入 OS 的位点(数据在 OS page cache 但未必落盘);flush LSN 是已调用 fsync 真正持久化到磁盘的位点。WAL-before-data 原则要求数据页刷盘前,对应的 WAL 必须先达到 flush。同步提交时事务要等 flush LSN 到达提交记录的位点;异步提交则只需 write 到位即可返回,由 walwriter 稍后异步 flush。

autovacuum 基于什么公式触发对更新/删除产生死元组的表的普通 vacuum?

核心触发条件是 dead tuple 数量超过阈值:threshold = autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * reltuples。默认 autovacuum_vacuum_threshold=50、autovacuum_vacuum_scale_factor=0.2,即垃圾记录约等于表行数 20% 时触发。对几乎只插入的表另有 insert vacuum 触发(autovacuum_vacuum_insert_threshold 与 autovacuum_vacuum_insert_scale_factor)。此外,若表的 relfrozenxid 或 relminmxid 太老,即使 autovacuum 被关闭,系统也会为防 wraparound 强制启动 autovacuum。

autovacuum_vacuum_max_threshold(PG18 新增)解决什么问题?

它给 autovacuum 触发增加了一个绝对上限:无论表多大、scale_factor 如何,dead tuple 数量一旦达到这个上限就触发 vacuum。目的是解决大表的自动垃圾回收频率过低问题——旧逻辑 threshold + scale_factor*reltuples 对大表(如十亿行)会算出极高的触发点,导致垃圾长时间累积。有了 max_threshold,DBA 可以给一个硬上限,避免大表膨胀失控。

bgwriter 和 checkpointer 的职责边界分别是什么?bgwriter 的 clock-sweep 是什么?

bgwriter 负责在后台定期扫描 shared buffer,把 LRU 里的脏页写出(BgBufferSync),减轻 client backend 需要自己找干净页时的同步刷脏压力;它用 clock-sweep 算法循环扫描 buffer 描述符,按一定速率推进写脏。checkpointer 则负责在 checkpoint 时把 dirty page 刷盘并写 checkpoint 记录,保证崩溃恢复能从一个一致点开始。bgwriter 是平滑、预防性的刷脏,checkpointer 是周期性、强一致性的刷脏。

bgwriter 相关参数 bgwriter_delay、bgwriter_lru_maxpages、bgwriter_lru_multiplier 各控制什么?

bgwriter_delay 是 bgwriter 两轮扫描之间的休眠间隔(默认 200ms)。bgwriter_lru_maxpages 是每轮最多写出的脏页数上限。bgwriter_lru_multiplier 是乘数,与最近一段时间的缓冲请求数相乘估算本轮应写的页数目标,再受 lru_maxpages 封顶。调大这些参数让 bgwriter 更激进地刷脏,可减少 backend 的同步刷脏等待,但过度会与业务争 IO。

block wand 剪枝如何缓解 Top K 简单查询的性能噩梦?

Top K 查询(ORDER BY … LIMIT K)如果列无索引,PG 传统上要做全表排序或堆排序,扫描成本高。block wand(块魔杖)剪枝利用’块级上界’思想:先按 block 统计每个数据块内目标列的最大/最小值(或上界信息),排序时优先处理可能包含 Top K 值的块、跳过不可能进入 Top K 的块,从而大幅减少需要扫描和排序的数据量,缓解 Top K 查询的全表扫描噩梦。

drop column 之后 VACUUM FULL 能释放 pg_attribute 里的 dropped column 元数据吗?为什么 1600 列上限无法通过 drop column 突破?

不能。drop column 只是逻辑删除,pg_attribute 中的行仍然保留(attnum 不回收、attisdropped 标记),VACUUM FULL 只回收旧值占用的物理空间,不会删除 dropped column 的元数据。因此 1600 列的硬限制(源于 tuple 最大宽度和 pg_attribute 行数)无法通过反复 drop column 来绕过——即使 drop 后再 vacuum full,pg_attribute 里仍有这些 dropped column 记录,attnum 计数不会回落,新增列仍会受 1600 上限约束。

freeze 事务号年龄降不下来时,应该优先排查什么?

应优先排查实例中最老的事务:两阶段事务(2PC / prepared transaction)、长查询(long query)、长事务(long xact)。因为 relfrozenxid/datfrozenxid 年龄能否推进取决于是否存在很老的事务快照:只要存在未提交的 prepared transaction 或长时间运行的事务,其 xmin 就会卡住 freeze 边界,导致年龄降不下来。处理方法是找到并结束这些最老事务(COMMIT/ROLLBACK PREPARED、杀掉长事务),再执行 VACUUM FREEZE 推进边界。

freeze 的本质是什么?XID 为什么必须循环使用、为什么需要 freeze 防止 wraparound?

XID 是 32 位 uint32,最多 43 亿个事务号,用完必须循环复用。PG 把 XID 空间看作圆,‘frozen xid’ 是圆上一个点,顺时针是过去(已分配)事务号、逆时针是未来(可分配)事务号。一旦 XID 走过半圆边界(消耗约 20 亿事务),老 XID 会被误判为’未来事务’,其 tuple 会从 MVCC 视野里’消失’。freeze 的防御机制就是 autovacuum 周期性扫表,把超过冻结边界的 tuple 标记为 HEAP_XMIN_FROZEN(把 t_xmin 替换成 FrozenTransactionId=2),标记后该 tuple 永远可见、不再参与 MVCC 判断,同时推进 relfrozenxid/datfrozenxid 边界。freeze 的本质不是清理老数据,而是防止老 XID 跨过半圆变成未来。

full_page_write 和 FPI(Full Page Image)是什么?为什么要默认开启?

full_page_write=on 时,checkpoint 后每个 page 第一次被修改时,WAL 记录里会写入该 page 的完整镜像(full-page image, FPI),而不仅是增量变化。原因是崩溃恢复时若页面只写了部分(torn page),没有完整镜像就无法还原。FPI 是 WAL 体积膨胀的主要来源,但换来可靠性;代价是性能、存储空间和稳定性开销。wal_compression 可对 FPI 压缩(PG15 起支持 pglz/zlib/lz4/zstd)。

greenplum 的 append only column store(AOCO)是什么?

Greenplum 的 AOCO(Append-Optimized Column-Oriented)是列式追加写存储:数据按列组织、以 append-only 方式写入,每列单独存一个文件并独立压缩,配合元数据里的 min/max 等统计做 segment 裁剪。它面向批量加载和 OLAP 扫描,支持列级压缩,适合数仓场景。追加写避免了 in-place 更新的随机 IO,但删除/更新需要额外的可见性处理(如 bitmap 或 AO 表的隐式版本)。

maintenance_work_mem 和 autovacuum_work_mem 在垃圾回收中起什么作用?设太小会有什么后果?

这两个参数控制 vacuum 记录垃圾 tupleid 的内存上限。vacuum 扫描表时把 dead tuple 的 TID 存进这块内存(tupleid 为 6 字节,1GB 约可存 1.7 亿条),内存占满后暂停表扫描、转去扫描索引按记录的 TID 清理死索引项,清完再回到断点继续扫表。若设得太小,索引会被反复扫描多次,浪费 IO 和时间。9.4 之前用 maintenance_work_mem,9.4 及之后可用 autovacuum_work_mem,未设置则回退到 maintenance_work_mem。判断依据是 autovacuum 日志里的 index scans 次数:超过 1 说明内存不够,可把 autovacuum_work_mem 乘以 index scans 来调大,或把 autovacuum_vacuum_scale_factor 除以 index scans 让 vacuum 更早触发。

parallel vacuum(PG13)如何并行清理一张有很多索引的表?

PG13 引入 parallel vacuum:当一张表有很多索引时,vacuum 的 index vacuum 阶段可以把不同索引分配给多个并行 worker 同时清理,主进程负责扫描 heap 收集 dead tuple TID。并行度受 max_parallel_maintenance_workers 和 autovacuum_max_parallel_workers 约束。索引多、单索引清理耗时的场景收益明显。

pg_control 控制文件的原子写和 CRC 校验机制是怎样的?

pg_control 保存实例的关键元数据(checkpoint 位置、WAL 位点、系统标识符、数据库状态、参数值等),是崩溃恢复的入口。为保证安全,它采用原子写(先写临时文件再 rename 或双写)和 CRC 校验:每次写 pg_control 时计算 CRC 并存储,读取时校验 CRC 是否一致,若损坏则拒绝启动(需用 pg_resetwal 重建)。CRC 用于检测半写/损坏的 pg_control。

pg_duckdb 和 pg_mooncake 在接入数据湖上的异同?

两者都是把 DuckDB 的列存/OLAP 能力接入 PostgreSQL:pg_duckdb 通过 DuckDB 引擎在 PG 里执行分析查询、读写 Parquet 等外部列存文件;pg_mooncake 更进一步面向数据湖,支持 Iceberg 等表格式和原生列存表。共同点是都依赖 DuckDB 的向量化列存执行来获得 OLAP 性能数量级提升;区别在于 mooncake 更强调数据湖生态(Iceberg/Parquet 管理),duckdb_fdw 更偏通用列存查询。

pg_mooncake 是什么?它如何让 PostgreSQL 具备数据湖/列存能力?

pg_mooncake 是 PostgreSQL 的数据湖/列存扩展,把 DuckDB 作为执行引擎内嵌,让 PostgreSQL 能直接在列存表(支持 Parquet、Iceberg 等格式)上做高性能 OLAP。它把 Postgres 的 SQL 接口与 DuckDB 的列存执行、向量化计算结合,用户可以在 PG 里创建列存表、查询外部 Parquet/Iceberg 数据湖文件,OLAP 性能相比行存有数量级提升。

pg_receivewal + 同步复制的 WAL 零丢失方案(mirror)是怎么实现的?

pg_receivewal 是一个流式接收 WAL 并落盘的工具,可把主库 WAL 实时镜像到远程目录。配合同步复制(synchronous_commit 和 synchronous_standby_names),主库提交需等 WAL 被备库/镜像端确认收到并持久化,实现 WAL 0 丢失。多层 mirror 时可用多个 pg_receivewal 或级联备库,把 WAL 同时镜像到多处,提升容灾可靠性。

pg_stat_autovacuum_scores 视图(PG19)解决了什么问题?它反映哪些维度的优先级?

它把 autovacuum 的优先级评分从黑箱变成可见。autovacuum 选择先处理哪张表是基于一个优先级评分,该评分由多个维度合成:XID score(事务号年龄)、MXID score(multixact 年龄)、vacuum score(死元组累积)、insert score(插入类 vacuum 需求)、analyze score(统计信息陈旧)。通过该视图可以看出一张表为什么被优先(或不被优先)处理,便于诊断 autovacuum 是否及时。

pg_stat_checkpointer(PG17)和其 num_done 字段(PG18)统计什么?

PG17 引入 pg_stat_checkpointer,把 checkpointer 的统计从 bgwriter 中分离出来,单独统计 checkpointer 的 checkpoint 次数、耗时、刷脏页数、写 WAL 量等。PG18 增加 num_done 字段统计实际完成的检查点次数,与 requested(请求的检查点数)区分,便于 DBA 了解 checkpoint 的触发与完成情况,排查 checkpoint 相关性能问题。

pg_stat_wal(PG14)和 track_wal_io_timing 参数提供什么统计?

pg_stat_wal 是 PG14 引入的实例级 WAL 统计视图,提供 wal_records(WAL 记录数)、wal_fpi(full-page image 数)、wal_bytes(WAL 字节数)等。track_wal_io_timing 参数开启后,可统计 WAL buffer 的 write 和 fsync 的 IO 等待时长,输出到 pg_stat_wal,帮助定位 WAL 落盘延迟瓶颈。PG15 起 pg_walinspect 插件可进一步分析 WAL 记录内容。

pg_surgery(PG14)用于修复什么?

pg_surgery 是 PG14 引入的 contrib 扩展,用于修复损坏的 tuple。它提供 force_freeze 和 remove 两种能力:force_freeze 可强制把 tuple 标记为 frozen(绕过正常的可见性检查),remove 可物理删除指定的 dead/corrupted tuple(通过 TID 指定)。它用于应急修复因损坏或 freeze 异常导致无法 vacuum 或无法访问的表,属于危险操作,只能由熟悉原理的 DBA 在明确目标下使用。

relfrozenxid 和 datfrozenxid 的年龄(age)代表什么?为什么 DBA 要重点监控它?

age(relfrozenxid) = 当前事务号 - relfrozenxid,表示自该表上次推进冻结边界以来已消耗的 XID 数量;datfrozenxid 是库级冻结边界。年龄越接近 20 亿(2^31),越接近 XID wraparound 危险区。防 wraparound 的数据安全优先级高于空间回收:一旦年龄失控,会触发数据库拒绝分配新 XID、强制进入单用户模式跑 freeze。所以 DBA 监控 age(relfrozenxid)/age(relminmxid) 比监控死元组更重要,应优先保证 freeze 及时推进。

tuple deformation 优化是什么?为什么能提升 OLAP 场景性能 5-20%?

tuple deformation 指把 heap 元组(行格式)‘拆解’成列式内存布局、抽取所需列值的过程,是 OLAP 扫描中 CPU 开销的大头。PG18/19 对 tuple deformation 做了优化(减少冗余的字段定位、批量解列、利用 SIMD 等),让行存表在做列式聚合/过滤时,把行拆列这一步更快,从而在 OLAP 场景获得 5-20% 的性能提升。

update 时新 tuple 如何选择空闲 block 插入?

heap_update 先尝试在原 page 内找空间(若 HOT 或同页有 free space 则同页插入,形成 HOT 链或普通更新链);同页放不下时,通过 FSM 查找有足够 free space 的 page 插入新版本,并把旧版本的 t_ctid 指向新版本、设置 xmax。若 FSM 里没有合适页,则向表尾扩展新 block。选择空闲 block 的核心依赖 FSM 的空闲空间近似值。

vacuum 的 failsafe 模式是什么?什么情况下会触发?

failsafe 是 PG14 引入的防 wraparound 兜底机制,由 vacuum_failsafe_age 和 vacuum_multixact_failsafe_age 两个参数控制。当表的 relfrozenxid 或 relminmxid 年龄逼近危险区(接近 20 亿回卷边界)时,vacuum 进入 failsafe 模式:跳过部分索引维护、不受 cost-based vacuum delay 限制,把目标优先级切到尽快推进冻结边界、防止 XID wraparound 数据丢失。这是以牺牲部分空间回收质量为代价换取数据安全。

vacuum_truncate 参数/选项控制什么?

它控制 vacuum 结束时是否收缩文件大小(截断表尾的空页)。标准 vacuum 把页内空间标记为可复用,通常只在表尾有整页全空且能拿到锁时才截断。vacuum_truncate 设为 off 可禁止截断,避免截断引起的锁等待或 IO 抖动;PG18 把它作为独立 GUC 暴露,PG19 也可在 VACUUM 语句级别控制。对于不想让 vacuum 频繁锁表截断的场景,关闭它更合适。

wal_compression 支持哪些压缩算法?各版本演进是怎样的?

wal_compression 用于压缩 WAL 中的 full-page image(FPI),减少 WAL 体积。PG15 起支持 pglz、zlib、lz4 三种,PG15 又加入 zstd;PG15 的 wal full page write 支持 lz4 压缩,PG15 同时支持 zstd 压缩 FPI。压缩 FPI 能显著降低 WAL 写入量,代价是少量 CPU 开销,适合 FPI 占比高、网络/磁盘带宽紧张的场景。

wal_recycle 和 wal_init_zero(PG12)适配 COW 文件系统解决什么问题?

PG12 引入 wal_recycle 和 wal_init_zero 两个 GUC。默认 PG 会复用旧的 WAL 文件并对其预置零(zero-fill),在 ZFS 等 COW(copy-on-write)文件系统上,预置零会导致大量无谓的写放大和空间占用。wal_recycle 控制是否复用 WAL 文件,wal_init_zero 控制是否在新建 WAL 文件时预置零。在 COW 文件系统上关闭这两个选项可避免写放大。

walwriter 的调度逻辑是什么?wal_writer_delay 和 wal_writer_flush_after 各控制什么?

walwriter 是后台进程,负责周期性地把 WAL buffer 刷到磁盘。它在一个主循环里休眠 wal_writer_delay(默认 200ms)后被唤醒,调用 XLogBackgroundFlush() 把累积的 WAL 写到 OS 并尝试 flush。wal_writer_flush_after 控制累计写出多少字节后强制 flush。异步提交最坏情况下会丢失约三倍 wal_writer_delay 时间内的已提交事务,因为 walwriter 的唤醒周期就是这个 delay。

zedstore 是什么?它的行/列混合存储思路是怎样的?

zedstore 是 PostgreSQL 基于 access method API 的列存/行列混合存储引擎实验项目。它把表按列族组织,支持行式与列式混合存储:把频繁一起更新的列放行式、把用于分析的大字段列放列式,兼顾 OLTP 更新与 OLAP 扫描。它通过 PostgreSQL 的 table access method 接口实现,可替换默认 heap AM。

为什么 VACUUM FULL / CLUSTER 需要 ACCESS EXCLUSIVE 锁?PG18 的 CONCURRENTLY 版解决了什么?

VACUUM FULL 和 CLUSTER 都是整表重写操作,会锁表、阻塞并发读写,且需要额外磁盘空间保存新副本。PG18 引入 VACUUM FULL / CLUSTER CONCURRENTLY,把重写过程改成类似在线重写的并发方式,降低对业务的阻塞影响,使得表收缩可以在接近在线的状态下完成。

为什么 VACUUM 不仅清理 dead tuple,还要清理 CLOG?

CLOG(Commit Log)记录每个事务的提交状态(每事务 2 bit),事务号推进后旧事务的提交状态最终可以被 ‘冻结覆盖’。VACUUM 在推进 freeze 边界(relfrozenxid 前进)时,意味着比该边界更老的 XID 的提交状态不再需要保留,对应的 CLOG 页可以被截断/回收。因此 vacuum 推进 freeze 的同时也把 CLOG 的过期页清理掉,防止 CLOG 无限增长;如果 freeze 长期不推进,CLOG 也会随之膨胀。

为什么 autovacuum 对大热表经常触发太晚?如何按表设置更合理的参数?

默认 autovacuum_vacuum_scale_factor=0.2 意味着一张估算 10 亿行的表要累积到约 2 亿 dead tuples 才触发普通 vacuum,除非被 autovacuum_vacuum_max_threshold 截住或按表设置更小阈值。热表应按业务写入速率和可接受膨胀窗口设置表级 storage parameters,例如调小 autovacuum_vacuum_scale_factor(如 0.01)、调低 threshold,必要时提高 worker 数、cost limit 或在低峰期手工补充 VACUUM,而不是盲目提高全局 autovacuum 频率。

为什么更新表的索引列会破坏 HOT 更新机会、加剧膨胀?

HOT(Heap-Only Tuple)更新要求更新的列不在任何索引中。若只更新非索引列,旧 tuple 的新版本可留在同一 page、旧版本通过 t_ctid 链指向新版本,索引无需新增条目,从而避免索引膨胀,VACUUM 也不必清理索引。一旦更新了索引列(或表上有表达式索引等),HOT 前提被破坏,每次 update 都要在索引里插入新 entry、留下旧 entry 变 dead,索引垃圾增多,VACUUM 清理成本也随之上升。因此高频更新表应减少不必要索引、避免频繁更新索引列。

为什么说 PostgreSQL 想成为 HTAP 数据库还缺一个存储引擎?

PostgreSQL 的默认 heap 是行存,适合 OLTP,但 OLAP 大表扫描需要列存、向量化、压缩和物化/冷热分离等能力。虽然已有 pg_duckdb、pg_mooncake、zedstore、OrioleDB 等实验或扩展引擎,但社区内核长期没有一个内置、成熟、可插拔的列存/混合存储引擎来无缝支撑 HTAP。行存到列存的转换(tuple deformation)、列存更新、混合事务/分析的一致性是主要挑战,所以’还缺一个存储引擎’。

为什么说 PostgreSQL 的 undo 存储引擎可能不那么重要了?

undo 存储引擎(如 zheap/zedstore、OrioleDB 的 undo)的设计动机之一是消除 heap 的死元组和 vacuum 压力。但文章认为,随着社区对 vacuum 的持续优化(并行 vacuum、failsafe、TidStore、更智能的 autovacuum 优先级、64 位 XID 减少 freeze 焦虑等),heap+MVCC 的痛点被逐步缓解,加上 64 位 XID 消除了 wraparound 风险,undo 引擎相对 heap 的收益空间变小,因此其必要性下降。

什么是 hint bit?为什么 CLogControlLock 会在大并发下成为风暴瓶颈?

hint bit 是 tuple header 里缓存的事务提交/回滚状态位。第一次读取某个 tuple 时,PG 需要查 CLOG 确认其 xmin/xmax 对应事务是否提交,确定后把结果以 hint bit 形式写回 tuple header(HEAP_XMIN_COMMITTED 等),后续读取就不再查 CLOG。当大量 tuple 尚未被 hint bit 标记、又集中在短时间内被访问时(例如刚导入大批数据后),所有会话都要访问 CLOG,争用 CLogControlLock 轻量锁形成风暴。PG11 通过批量读取 CLOG、减少锁竞争等内核层优化缓解了这一问题。

分组提交(group commit)是什么?它对高并发写有什么价值?

分组提交指多个并发事务的 WAL 记录被合并到同一次 fsync 中一起刷盘:一个事务发起 XLogFlush 时,把缓冲区内其他事务也已就绪的 WAL 一并刷盘,大家共享一次 fsync 的开销。这样在高并发写场景下,单事务的平均 fsync 成本大幅下降,提升提交吞吐。fsync 是写路径上最贵的操作之一,分组提交能有效摊薄这一成本。

同步提交与异步提交在 WAL flush 行为上的区别是什么?

同步提交(synchronous_commit=on)时,事务提交要等 WAL 记录 fsync 到磁盘(flush LSN 到达提交位点)才返回,保证已提交事务不丢。异步提交(synchronous_commit=off)时,提交只需 WAL 写入 OS(write LSN 到位)即可返回,由 walwriter 稍后异步 flush,最坏情况下可能丢失约三倍 wal_writer_delay 时间内的已提交事务,但能显著降低提交延迟、提升吞吐。业务若通过查询 wal flush lsn 控制最终一致,可兼顾性能与一致性。

垃圾回收与膨胀的根因是什么?UPDATE/DELETE 为什么制造垃圾?

根因是 MVCC 的追加写模型:UPDATE/DELETE 不立即删除旧行版本。heap_update() 会准备新 tuple 写入页面(新页面或同页),在旧版本上设置 xmax 并把旧版本 t_ctid 指向新版本;若满足 HOT 条件,旧版本标记 HOT-updated、新版本标记 heap-only。旧版本何时可清理,取决于系统里是否还有更老的快照、事务、复制槽或逻辑复制保留需求。只要更新频率高于回收频率,或回收被阻塞,page 内部就累积不可立即复用的旧版本,形成膨胀。

如何得到某个事务 commit 或 abort 时的 WAL LSN 位置?

可以通过查询事务提交时写入的 WAL 位点来获取。方法之一是使用 pg_current_wal_insert_lsn() 或 pg_current_wal_flush_lsn() 在事务提交前后采样;更精确的做法是利用 pg_waldump 或 pg_walinspect 分析 WAL 记录,找到该事务的 commit/abort 记录对应的 LSN。txid_current() 与 WAL 位点的关联需要借助 WAL 记录中携带的 xid 信息。

如何诊断一张表是否膨胀?pgstattuple 能提供什么信息?

pgstattuple 会扫描整个关系,返回 tuple 数、dead tuple 数、free space、total_len 等物理统计,适合确认膨胀程度。但它是全表扫描,不适合高频扫大表。更轻量的方式是结合 pg_stat_all_tables 的 n_dead_tup、last_autovacuum、autovacuum_count、vacuum_count,以及 pg_relation_size、pg_freespace/VM 信息综合判断。好的膨胀检查 SQL 应同时看死元组占比、表大小与 VM all-visible 比例,并排查长事务、复制槽等阻塞者。

如何配置 PostgreSQL 归档,并自动删除 N 天前的归档 WAL?

通过 archive_mode=on + archive_command 把完成的 WAL 归档到目标目录(archive_command 里可用 %p 源、%f 文件名等占位符)。自动清理过期归档通常由外部脚本完成,例如用 find 按 mtime 删除 7 天前的归档文件,配合 crontab 定时执行。核心是保证归档先成功(archive_command 返回 0)再允许 WAL 被复用,清理时也要确保下游(PITR 备份、复制槽)不再需要。

异步提交场景下,业务如何通过查询 wal flush lsn 控制最终一致?

异步提交下事务返回时 WAL 可能尚未 fsync。业务若需要确认某笔已提交数据已真正落盘(例如跨库对账、下游消费),可以查询当前 wal flush LSN(如 pg_current_wal_flush_lsn()),并与目标事务提交时的 WAL 位点比较:当 flush LSN 大于等于该事务的提交 LSN 时,说明该事务的 WAL 已持久化。这样在不牺牲异步提交吞吐的前提下,按需实现最终一致确认。

标准 VACUUM 与 VACUUM FULL 的区别是什么?为什么 VACUUM 后表文件往往不缩小?

标准 VACUUM 的目标是稳态空间复用而非最小化文件:它删除表和索引里的 dead row versions,把空间标记为未来可复用,通常不把空间归还给操作系统,除非表尾有整页全空且能拿到锁才截断。VACUUM FULL 则重写整张表把文件压缩到更小,但需要 ACCESS EXCLUSIVE 锁,且需要额外磁盘空间保存新副本直到完成。所以大删除后表文件不缩小是预期行为,关键看后续写入是否复用了这些空间;若业务不会再写入同等规模数据,才考虑 VACUUM FULL、CLUSTER、分区 drop/detach、在线重写工具(如 pg_repack)。

标准 VACUUM 的三个阶段分别是什么?为什么不能一发现死元组就立刻释放 heap line pointer?

vacuumlazy.c 把 heap vacuum 分为三阶段:1) 扫描 heap page,剪枝和冻结 tuple,把需要从索引删除的 dead tuple TID 存入 TID store;2) 扫描索引,删除这些 TID 对应的 dead index entries;3) 回到 heap page,把对应 LP_DEAD line pointer 标记为 LP_UNUSED 让页内空间可复用。不能立即释放的原因:索引里可能还有指向该 TID 的条目,必须先批量清理索引,再回 heap 释放 line pointer,否则会出现索引指向已复用空间的不一致。

遇到 ‘database is not accepting commands to avoid wraparound data loss’ 或 ‘uncommitted xmin before xid cutoff needs to be frozen’ 报错怎么处理?

前者表示数据库已进入防 wraparound 保护模式,拒绝分配新 XID,要求立即 vacuum 推进冻结边界;这是数据安全告警,绝不能当作普通后台任务杀掉。处理方法是先解除阻塞者(长事务、idle in transaction、prepared transaction、复制槽、逻辑复制保留),然后对年龄最高的库/表执行 VACUUM(必要时 VACUUM FREEZE),让 relfrozenxid/datfrozenxid 前进。后者(found xmin before relfrozenxid)通常是数据损坏或 freeze 处理异常,需按报错定位具体表,检查是否有孤儿事务或需用 pg_surgery 之类工具修复。

金仓 V9 的 64 位事务号改造最可能采用哪种实现?为什么默认参数值没变?

金仓 V009R002C016 公开宣称支持 64 位 XID,但实现未公开。它把 VACUUM 相关参数(如 vacuum_freeze_table_age 等)数据类型升级为 int64 但默认值不变(仍是 200000000/400000000 等),只扩大上限。据此推测它走的是’分配空间 64 位、tuple header 仍 32 位’的混合方案(类似 xid8 思路):分配号 64 位不再循环,但 tuple header 的 t_xmin 仍是 32 位、仍需 freeze,只是 freeze 频率大幅降低。默认值不变是给 DBA 留一个’老 PG 运维经验继续管用’的兼容层,降低迁移心智成本。

长事务为什么即使不锁住目标表,也会阻止 VACUUM 清理旧版本?

VACUUM 判断一个 dead tuple 能否删除,取决于它是否对所有现存事务都不可见。系统里只要有很老的快照(backend_xmin)、prepared transaction、复制槽 xmin/catalog_xmin 或逻辑复制保留需求,VACUUM 看到的很多旧版本仍是 recently dead,不能删。因此长事务、idle in transaction 即使不持有目标表锁,也会因其老快照而阻止 VACUUM 推进清理边界,导致 dead tuple 持续累积、膨胀。排查膨胀时应先找这些阻塞者。