TOAST是在PostgreSQL中引入的,用以处理超过8KB页面大小的行数据,通过将大型列压缩到单独的物理存储中。当逻辑复制通过WAL流发送更改时,REPLICA IDENTITY设置决定了包含哪些旧元组值。默认的REPLICA IDENTITY DEFAULT仅发送主键,而REPLICA IDENTITY FULL则发送完整的旧行图像。
当一个表包含超过~2KB并被压缩到TOAST中的JSONB或TEXT列时,仅修改非TOAST列的UPDATE操作可能不会为WAL记录提取外部TOAST值。逻辑解码过程会跳过未更改的TOAST指针以减少I/O,导致订阅者在冲突解决期间收到这些大字段的NULL或缺失值。
切换到REPLICA IDENTITY FULL会强迫PostgreSQL在每次提交中将完整的旧元组包含在WAL记录中,明确地从外部存储获取所有TOAST值。虽然这保证了UPSERT操作的数据完整性,但会显著增加WAL流量——对于主要包含宽JSONB表,通常增加300-500%,因为每个UPDATE都必须记录行的完整前图像。
一个金融交易平台需要将订单薄快照从主PostgreSQL 15集群复制到数据仓库,以进行合规报告。market_data表存储仪器标识符和包含深度信息的大型JSONB有效负载(10-50KB)。复制使用pglogical与REPLICA IDENTITY DEFAULT(仅主键)。数据仓库端的ETL过程试图执行UPSERT操作以维护一个缓慢变化的维度表,需要旧的JSONB值来计算审计日志的增量变化。
在高交易量的交易期间,当订单薄频繁更新但JSONB有效负载保持不变时,逻辑复制流发送的UPDATE记录仅包含主键和新元组数据。由于UPDATE语句仅修改了updated_at时间戳列,因此未包含变更集中的TOAST旧的JSONB值。ETL过程无法访问更新前的JSONB状态,导致无法计算审计轨迹的精确价格变动增量,违反了MiFID II合规要求。
解决方案1:切换到REPLICA IDENTITY FULL 这种方法将强制发布者为每次UPDATE将完整的旧行图像写入WAL,包括来自TOAST存储的完整JSONB内容。优点包括保证数据完整性和简单的实现,无需更改架构。然而,缺点显著:WAL生成将增加约400%,考虑到50KB的有效负载,可能会导致主机上的磁盘空间耗尽,并增加到仓库的网络延迟。对于每秒处理10,000个更新的表,这被认为对生产稳定性风险太大。
解决方案2:应用级别的日志记录与单独的历史表
团队考虑在主服务器上创建一个触发器,在更新之前将旧的JSONB值复制到单独的market_data_history表中。优点是逻辑复制可以单独复制这个历史表,避免在主表中遗漏TOAST,同时保持主表的WAL足迹较小。缺点包括在主表上的重复写入开销(增加事务延迟)、以2倍速度增长的额外存储需求以及在使用事务ID和时间戳将历史记录与主表更改关联的ETL逻辑中的复杂性。
解决方案3:使用包含JSONB哈希的REPLICA IDENTITY与覆盖索引
这种策略涉及在md5(jsonb_column::text)上创建一个函数索引,并将该哈希包含在一个复合REPLICA IDENTITY索引中。优点是对JSONB内容的更改可以通过WAL中的哈希变化来检测,而无需发送完整有效负载。缺点是无法检索实际的旧值(仅其哈希),这不足以满足显示确切更新前状态的合规要求,以及在高频率更改的表上维护索引的开销。
团队选择了解决方案2(应用级日志记录),但进行了修改。他们利用PostgreSQL在14+版本中提供的JSONB部分更新优化,并实现了一个BEFORE UPDATE触发器,仅存储更改的路径(diff),而不是完整的旧行。这减少了历史表的增长,同时确保了所有必要的前图像数据可用。他们在主表上保留REPLICA IDENTITY DEFAULT以避免WAL膨胀,将ETL引导至对历史表进行审计重建的连接。
复制流量保持稳定,防止主存储压力。ETL过程通过将当前行状态与历史表中存储的差异合并成功重建完整的审计轨迹。合规性通过仅增加15%的主存储(而不是使用REPLICA IDENTITY FULL的400%)和对事务吞吐量的最小影响实现。
为什么PostgreSQL的逻辑解码在列被修改时仍然省略TOAST值?
许多候选人假设任何UPDATE自动提取所有TOAST值到WAL中。然而,PostgreSQL仅在执行者实际读取数据以进行修改时才进行“元组未TOAST”。如果一个UPDATE修改了另一个列(例如,SET updated_at = NOW())而未在目标列表或WHERE子句中引用JSONB列,则TOAST指针保持不变,外部存储不会被访问。因此,WAL记录仅包含磁盘上的元组及其指针,而不是真正的数据。由于逻辑解码从WAL重建元组,而不访问堆或TOAST表的旧版本,因此省略的值在变更流中显示为NULL。
REPLICA IDENTITY FULL与HOT(仅堆元组)更新的交互如何?
候选人常常忽略REPLICA IDENTITY FULL会禁用表的HOT更新。HOT更新允许PostgreSQL在相同数据页面内链接行版本,而无需更新每个索引条目,前提是没有任何索引列发生变化。当REPLICA IDENTITY FULL处于活动状态时,每个UPDATE必须记录整个旧行图像以进行复制,这要求系统按其完整内容唯一识别该行。这破坏了HOT优化,因为逻辑复制需要完整的元组比较数据,从而强制对每个行版本进行索引更新,即使修改的是未索引的列。因此,具有此设置的表经历了更高的索引膨胀和增加的I/O,这对于高频率变更的表来说是一个重要的权衡。
TOAST压缩与PostgreSQL的WAL压缩之间的区别是什么,以及在逻辑复制中它们如何交互?
这个问题区分了深层系统知识与表面理解。TOAST压缩使用LZ4或PGLZ减少行大小,然后将大型列存储在外部表中。WAL压缩(通过wal_compression=lz4启用)压缩写入WAL的完整页面图像以提高崩溃恢复效率。然而,当使用REPLICA IDENTITY FULL时,发送到逻辑解码的旧元组数据在WAL记录压缩存储之前被提取。因此,逻辑解码器接收到的TOAST数据是未压缩的(如果已提取),而实际的物理WAL文件可能将其存储为压缩,如果它是完整页面图像的一部分,这会影响网络带宽与磁盘I/O的不同。