ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

DeepSeek总结的大规模分区四个 PostgreSQL 表不同策略

DeepSeek总结的大规模分区四个 PostgreSQL 表不同策略 2026年9月17日作者Uche Nnodim博客Uche 的 Planet PostgreSQL大规模分区四个 PostgreSQL 表没有一刀切的答案真实数据而非惯例如何为一个生产数据库中最大的四个表塑造了四种不同的 PostgreSQL 分区策略以及我们在两个 bug 到达生产环境之前捕获了它们。关键要点为一个生产数据库模式中最大的四个表设计了分区策略刻意没有对四个表使用相同的方法。外键依赖分析揭示了其中两个表上分别有 35 和 59 个依赖关系这是一个硬性结构约束决定了迁移顺序和复杂性而非团队偏好问题。真实生产数据揭示了严重的倾斜仅一个事件就占了一个表全部数据量的约 13%这促成了一个混合分区设计而非天真地平均分割。构建了一个零停机迁移模式结合了基于触发器的实时同步和一个自动取消调度的批量回填因此迁移过程中不会遗漏任何写入。两个与同一个父表有多个外键关系的表共享了一个辅助列悄无声息地用另一个关系的数据破坏了其中一个关系的数据。起点这项工作揭示了一个更长远的议题该平台最大的四个表正接近这样的规模——正常的维护、索引重建甚至常规查询都开始感受到单个扁平表中数亿行的重量。原则上分区是显而易见的答案。在实践中“就把它分区”不是一个策略而是一个方向而实际的策略完全取决于特定表的形状和查询方式。所以在写下任何一条CREATE TABLE语句之前我们为四个表中的每一个问了两个问题。第一模式中是否有任何其他东西通过外键依赖这个表第二数据本身是否有一个自然的、均匀分布的分区键还是隐藏着那种会让平均分割比不分区表现更差的倾斜为每个表找到正确的策略而不是所有表共用一个策略在确定方法之前检查依赖关系的本能将本可以是一个统一、便利的计划变成了四个真正不同的、基于证据的计划。在确定任何迁移计划之前统计有多少其他表通过外键依赖一个候选表SELECTcount(*)FROMpg_constraintWHEREcontypefANDconfrelidGlobalEvent::regclass;四个表中有两个返回干净结果完全没有传入外键使它们成为自包含、低风险的首次迁移候选。另外两个则讲述了完全不同的故事整个模式中分别有 35 和 59 个依赖关系。这不是事后再处理的细节它改变了迁移的整个形态每个依赖表的外键都需要加宽以包含新的分区键分批回填然后才能重新指向所有这些都要在实际分区工作能够安全开始之前完成。对于工作区workspace或租户标识符看起来是自然分区键的两个表我们没有假设平均分割会奏效。我们提取了真实生产数据并进行了检查。检查基于工作区的分区是否真的会均匀分布SELECTcount(*)ASdistinct_workspaces,quantile(0.5)(cnt)ASmedian_rows_per_workspace,quantile(0.95)(cnt)ASp95_rows_per_workspace,max(cnt)ASlargest_workspace_rows,sum(cnt)AStotal_rowsFROM(SELECTeventId,count()AScntFROMsandbox.event_minGROUPBYeventId);结果很决定性在 273 个不同的工作区中中位数工作区持有约 20,000 行但最大的单个工作区持有超过 430 万行是中位数的 200 多倍仅此一个就占了整个表数据量的约 13%。对工作区 ID 进行简单的基于哈希的分割会产生一个极度超大的分区和几十个大小舒适的分区这恰恰违背了分区的目的而且是对最重要的那个租户。工具包的其余部分从这些证据中浮现出两种结构上不同的策略在四个表中一致应用。按日期进行范围分区大小根据实际增长确定。对于两个没有传入依赖且有明确时间维度的表月度范围分区是自然的选择但不是从第一天起就采用统一的月度方案。真实的行数数据显示出稳定的、多年的增长曲线最早几个月每个月只有几百行最近几个月有数百万行。统一的月度分割会产生大部分空的早期分区和极度超大的近期分区。最终设计使用一个宽泛的“遗留”legacy分区来吸收稀疏的早期历史只在数据证明合理的时点才切换到真正的月度分区。混合列表和哈希分区大小根据倾斜确定。对于租户键控的表修复方案是一种混合方案为已确认的最大工作区提供专用的、独立的分区这样任何单个租户的数据都不会主导一个共享桶。普通工作区的长尾则均匀分布在哈希桶中大小根据它们自己更为平坦的分布确定。以这种方式隔离最大的工作区将剩余的倾斜比从 200 多倍降低到约 32 倍这是一个真实的、经过测量的改进而非猜测。零停机迁移而非维护窗口。每次迁移都遵循相同的模式在活动表旁边构建新的分区表附加一个触发器将每个插入、更新和删除实时镜像到新表中然后按一个自动停止的调度对历史行运行批量回填一旦检测到没有剩余要复制的内容就会停止。活动表从不停止服务流量而切换本身是一个单一、简短的事务只是重命名两个表并将新表提升到位。防范空操作写入大小根据每个表的实际更新率确定。同一个实时同步触发器可以无条件写入或者先检查是否真的有任何变化。那个保护有真实的成本因此我们没有默认在所有地方都添加它而是在决定之前检查了每个表的实际更新强度。逐个表决定空操作写入保护是否值得其成本SELECTrelname,n_tup_upd,n_live_tup,ROUND(n_tup_upd::numeric/NULLIF(n_live_tup,0),2)ASupdates_per_live_rowFROMpg_stat_user_tablesWHERErelnameGlobalActivity;一个表返回每活动行 0.29 次更新足够低以至于保护的开销不值得添加。另外三个返回显著更高一个高达每活动行 7 次更新确认了保护在那些特定表上会多倍地回本。在所有地方应用相同的修复无论底层表是否真的需要它会是更容易的路径也是错误的路径。一个关于做对事情的诚实故事在这项工作中两个真实的 bug 在任何一个到达生产环境之前被捕获了两者都值得平实地描述因为捕获它们正是仔细而非快速进行这次迁移的实际价值。第一个是结构性的而且容易被忽略。PostgreSQL 在外键约束和触发器被创建的那一刻就将它们绑定到表的底层身份identity上而不是绑定到表名上。迁移计划的一个早期版本在最终的“重命名并切换”步骤之前在依赖表上添加了加宽的外键约束。这个顺序在纸面上看起来是正确的。在实践中一旦活动表被改名到一边新建的分区表被提升到其位置任何先前创建的约束都会悄无声息地仍然绑定到旧的、现已退役的表而不是实际服务流量的那个表。修复很简单就是重新排序将约束创建移到切换之后但发现它需要追踪 PostgreSQL 在重命名过程中如何精确地跟踪对象身份而不仅仅是相信 SQL 运行没有错误。第二个更狭窄但同样真实。两个依赖表各自有多个指向同一个父表的外键关系例如一个表将两条不同的内容记录相互链接。自然的方法——用一个辅助列跟踪迁移的分区键值——对于只有一个关系的表工作得很好。对于各自有两个关系的两个表两个关系都指向同一个共享辅助列。因此填充第二个关系的值会悄无声息地覆盖第一个的值。没有任何错误。修复是给每个不同的关系一个自己唯一命名的辅助列在它运行在任何接近真实数据的地方之前逐列重新检查实际生成的 SQL 来确认。这两个 bug 从外部都不会在切换后很久才可见那时看起来配置正确的参照完整性检查会悄无声息地什么都不强制执行或者一个关系的数据会以任何错误消息都永远不会暴露的方式出错。在一个生产行被移动之前捕获两者正是为什么这种迁移要以书面形式规划、逐行审查并针对真实依赖数据进行测试而不是因为语法有效就假定它正确。与我们讨论零停机 PostgreSQL 迁移结语这四个表最终采用了不同的分区策略而这个结论之所以站得住脚只是因为对每个表中的数据都运行了倾斜分析。如果你的团队正在审视一个仅仅超出了扁平模式承载能力的表正确的分区方案很少是第一个想到的那个而且它几乎从不是数据库中每个表都相同的方案。这正是 Stormatics 所做的证据优先的迁移规划。
返回列表