聚合函数慢不一定靠加索引解决,全表扫描类聚合需索引覆盖才有效;物化视图在PG中可用但需手动刷新,MySQL无原生支持;预计算表应匹配查询粒度、设唯一索引并避免冗余字段;高基数、动态筛选或列存引擎场景不宜预计算。

聚合函数慢,是不是该加索引?
不是所有聚合都靠索引提速。像 COUNT(*)、SUM()、AVG() 这类全表或大范围扫描的聚合,如果底层表没走索引覆盖(即查询字段和过滤条件都能被一个索引包含),加普通索引效果极小,甚至无效。更常见的情况是:明明有索引,执行计划里还是出现 Seq Scan 或 Table Scan —— 因为优化器判断走索引回表成本高于直接扫堆。
- 先用
EXPLAIN (ANALYZE, BUFFERS)确认是否真在扫描全表,以及是否命中索引覆盖 - 对高频聚合字段(如
status、created_date)建复合索引时,把WHERE条件列放前面,聚合字段放最后(例如CREATE INDEX idx_orders_status_date ON orders (status, created_date),支撑SELECT COUNT(*) FROM orders WHERE status = 'paid' GROUP BY DATE(created_date)) - 避免在聚合字段上用函数(如
GROUP BY YEAR(created_at)),这会阻止索引使用;改用范围查询 + 表达式索引替代
物化视图真能用?PostgreSQL 和 MySQL 的现实差距
PostgreSQL 12+ 的 MATERIALIZED VIEW 支持刷新控制,但默认不自动更新,每次 REFRESH MATERIALIZED VIEW 是全量重算,锁表时间取决于数据量。MySQL 根本没有原生物化视图,所谓“物化”只能靠手动维护的汇总表,且需自行保证一致性。
- PostgreSQL 中,若聚合结果变更不频繁(如日报统计),用
REFRESH MATERIALIZED VIEW CONCURRENTLY可避免阻塞读,但要求物化视图必须有唯一索引 - MySQL 用户别依赖“物化视图插件”,稳定性差;老实用定时任务(
EVENT或外部调度)写入预计算表,并用INSERT ... ON DUPLICATE KEY UPDATE增量合并 - 物化视图/预计算表的命名要有业务语义,比如
sales_daily_summary,别叫mv_1,否则半年后没人记得它统计的是什么维度
预计算表怎么设计才不翻车?
预计算表不是简单把 GROUP BY 结果 INSERT INTO ... SELECT 一遍就完事。关键在粒度、更新机制和查询路由逻辑。
- 粒度要匹配最常查的维度组合:比如常查“按地区+月份+产品类目”的销售额,就建对应三级分组的预计算表,别只做“按月份”一级——否则联表补维度时性能反而更差
- 加
updated_at字段记录最后计算时间,并在应用层判断:若查询的时间范围早于该值,直取预计算表;否则退回到原始表实时算(避免脏读) - 务必给预计算表建主键或唯一索引(如
(region_id, year_month, category_id)),否则后续增量更新或去重会异常慢 - 不要在预计算表里存冗余明细字段(如用户姓名),只存聚合值和分组键;明细查原始表
哪些场景根本不该预计算?
预计算本质是用空间换时间,但空间和维护成本会被低估。当聚合涉及用户自定义筛选(如前端多条件动态下拉)、高基数分组(如 GROUP BY user_id)、或数据延迟敏感(如监控看板要求秒级更新),预计算不仅无效,还会引入一致性陷阱。
- 用户可任意输入日期范围、关键词、状态组合的报表页,预计算表无法覆盖所有组合,硬上会导致表爆炸(N个维度 → 2^N 张表)
- 分组字段基数 > 100万(如日活用户数),预计算表体积可能超过原始表,且
INSERT ... SELECT刷新一次要十几分钟 - 如果原始表已用列存引擎(如 ClickHouse、Doris)或启用了向量化执行(PostgreSQL 15+ 的
enable_vectorized_executor),聚合本身已很快,预计算收益趋近于零
真正省时间的地方,往往不在“算得快”,而在“少算几次”——比如把每秒跑一次的 COUNT(*) 改成 Redis 的原子计数,或者把小时级聚合缓存进内存表。预计算表只是工具之一,不是银弹。


















