SQL 架构复杂度的控制策略

8次阅读

表设计应拒绝大宽表,按业务变更频率拆分;外键需显式声明但避免跨服务强依赖;枚举字段须建独立表并加CHECK约束;视图仅用于简单过滤,复杂聚合用物化视图;索引须匹配真实查询模式,复合索引顺序关键;迁移脚本必须可重入且带验证。

SQL 架构复杂度的控制策略

表设计阶段就要拒绝“大宽表”

宽表在初期看似省事,但很快会成为查询性能、权限管理、变更风险的源头。字段超过 30 列的表,大概率存在职责混杂——比如把用户基本信息、订单统计、风控标签全塞进 users 表里。

  • 拆分依据不是“能不能 JOIN”,而是“业务变更频率是否一致”:用户资料每月改一次,而登录日志每秒写入,必须分离到 usersuser_login_events
  • 外键约束宁可显式声明,也不要靠应用层“保证一致性”;但也要避免跨微服务强外键,此时用逻辑关联 + 定期校验更实际
  • 枚举类字段(如 status)别用字符串编码,建独立 status_types 表并加 CHECK 约束,否则后续改名或扩值域时全库 UPDATE 风险极高

视图和物化视图的使用边界

视图不是银弹。普通 VIEW 只是保存 sql 文本,嵌套三层后,select * 可能触发全表扫描+重复计算,而你根本没意识到。

  • 简单查询封装(如过滤掉已删除记录)用 VIEW 没问题:CREATE VIEW active_orders AS SELECT * FROM orders WHERE deleted_at IS NULL
  • 涉及多表聚合、窗口函数、或高频访问的报表逻辑,优先考虑物化视图(postgresql 9.3+ 的 MATERIALIZED VIEW,或 mysql 用定时任务+汇总表)
  • 物化视图更新策略必须明确:是 REFRESH CONCURRENTLY(PG),还是容忍几分钟延迟?千万别在事务里调 REFRESH MATERIALIZED VIEW,它会锁整个视图

索引不是越多越好,而是要匹配查询模式

加索引前先看慢查询日志里真实出现的 WHEREJOINORDER BY 组合,而不是凭感觉给所有 idcreated_at 加索引。

  • 复合索引顺序至关重要:WHERE a = ? AND b > ? ORDER BY c 最佳索引是 (a, b, c),反过来就失效
  • LIKE '%abc' 无法走 B-tree 索引,这类搜索要么用全文索引(tsvector),要么前置生成反向字符串索引
  • 删除长期未被 pg_stat_all_indexes.idx_scan 记录使用的索引,它们只拖慢写入、增大备份体积

迁移脚本必须可重入且带验证

ALTER table ADD column 看似安全,但如果脚本重复执行,可能报错“column already exists”,导致自动化发布中断。

  • 所有 DDL 脚本开头加判断:DO $$ BEGIN if NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'orders'::regclass AND attname = 'paid_at') THEN ALTER TABLE orders ADD COLUMN paid_at TIMESTAMPTZ; END IF; END $$;
  • 变更后立刻跟验证语句,比如加了非空约束,就跑 SELECT count(*) FROM orders WHERE new_column IS NULL,不为 0 就中止后续步骤
  • 生产环境禁止直接 DROP COLUMN,先设为 NULL、删默认值、应用层停写、确认无读取后再真正删除

复杂度控制真正的难点不在技术选型,而在团队对“什么算合理拆分”的共识。一个字段该不该进这张表,往往卡在产品、后端dba 对同一业务术语的理解偏差上。这时候,比 SQL 更重要的是那张贴在白板上的实体关系草图,以及每次评审时敢问一句:“这个字段下次改的时候,会影响多少个服务?”

text=ZqhQzanResources