Hollis Varden:四个打平手的 PostgreSQL 争论
内容提要
作者在PostgreSQL 18.6上实测四条常见建议:IN、ANY与VALUES连接性能相当,ANY更利于预处理语句;text、smallint、enum存状态列大小几乎相同;NULL列几乎不占空间;count(DISTINCT)反而比GROUP BY子查询更快。结论是这些建议需用EXPLAIN在自有数据上验证,不能盲从。
延伸解读
实测环境与数据规模
文章基于 PostgreSQL 18.6 默认设置,在 2020 款 13 英寸 MacBook Pro(Intel i5-8257U、8 GB 内存)上测试。核心表 u 有 200 万行,包含 id、email、status 和时间戳。执行时间取自 EXPLAIN (ANALYZE, TIMING OFF),使用热缓存,丢弃首次运行,取剩余运行的中位数。这一设置意味着结论主要反映该硬件与数据量下的相对趋势,而非绝对性能。
IN、ANY 与 VALUES 连接:性能相当,ANY 更利于预处理
对于 1000 个 id 的查询,IN 列表、ANY 数组和 VALUES 连接的中位执行时间分别为 26 ms、19 ms 和 20 ms,但 IN 有一次运行达到 54 ms,波动较大,因此作者认为三者性能差异不显著。三者读取的缓冲区数完全相同(3001)。ANY 的实用优势在于:当数组作为单个参数传递时,语句文本不随 id 数量变化,对预处理语句和 pg_stat_statements 更友好。
状态列类型:text、smallint 与 enum 空间几乎相同
在 200 万行、三个状态值的测试中,text、smallint 和 enum 三种存储方式的表大小分别为 72.6 MB、73.0 MB 和 72.7 MB,索引均为 13.9 MB。原因是每行有 24 字节头部并按 8 字节对齐,2 字节的 smallint 和 7 字节的 'active' 最终占用相同空间;B-tree 去重也让索引大小一致。因此,短状态列的类型选择不应以节省空间为主要理由,而应基于约束和可读性。
NULL 列几乎不占空间,count(DISTINCT) 未必慢
100 万行中,十个全 NULL 整数列的表为 44 MB(每行 36 字节),而十个全填充列的表为 77 MB(每行 68 字节)。NULL 在行内不占空间,仅消耗空位图中的一个比特,因此稀疏列几乎免费,拆分表以节省空间未必必要。另外,在 200 万唯一 email 上,count(DISTINCT email) 中位耗时 1.95 秒,而改写为 GROUP BY 子查询后 count(*) 耗时 3.12 秒,改写反而更慢。作者强调这取决于执行计划和 work_mem,需用 EXPLAIN 在自有数据上验证。
Q&A
在 PostgreSQL 中,用 IN、ANY 还是 VALUES 连接来查询一批 ID 性能更好?
实测显示三者性能相当,读取的页面数完全相同。中位数差异在运行波动范围内,因此选择哪种形式主要取决于驱动程序的便利性。ANY 有一个实际优势:当数组作为单个参数传递时,语句文本保持不变,这对预处理语句和 pg_stat_statements 更友好。
PostgreSQL 中存储状态列,用 text、smallint 还是 enum 更省空间?
对于短状态值,三者占用的空间几乎相同。因为每行有 24 字节头部并填充到 8 字节,2 字节的 smallint 和 7 字节的 'active' 最终大小一样。索引也相同,因为 B-tree 去重会为每个值存储一次并附带行指针列表。选择类型应基于它允许你进行何种检查,而不是空间。
PostgreSQL 中 NULL 值会占用存储空间吗?
NULL 在行中不占空间,只在行的空位图中占用一个位。因此,大部分为空的可选列几乎不占空间,在考虑拆分表以节省空间之前值得了解这一点。
count(DISTINCT) 和 GROUP BY 子查询哪个更快?
在 200 万唯一邮箱的测试中,count(DISTINCT email) 中位数为 1.95 秒,而重写为 GROUP BY 子查询后中位数为 3.12 秒,重写反而更慢。但哪个更快取决于 GROUP BY 获得的计划和 work_mem,并且会随版本和设置变化。因此,这应该用 EXPLAIN 在自己的数据上验证,而不是盲目应用。
为什么有些 PostgreSQL 优化建议在代码审查中经常被重复,但实际效果可能相反?
因为有些建议听起来合理,但实际测量后可能发现是平局甚至相反。例如,IN、ANY 和 VALUES 连接性能相当;text、smallint 和 enum 存储状态列大小几乎相同;NULL 列几乎不占空间;而 count(DISTINCT) 反而比 GROUP BY 子查询更快。这些建议需要用 EXPLAIN 在自有数据上验证,不能盲从。
在 PostgreSQL 中,使用 ANY 传递数组参数对预处理语句有什么好处?
当数组作为单个参数传递给 ANY 时,无论 ID 数量多少,语句文本都保持不变。这对预处理语句和 pg_stat_statements 更友好,因为不需要为不同数量的 ID 重新解析或生成不同的语句。