使用dm_db_partition_stats获取过滤后行数,解决分页COUNT(*)性能问题
我太懂你这种被分页统计拖垮的无奈了——900万行的COUNT(*)要跑43秒,这对用户体验来说完全是灾难,更别说14亿行那种量级的场景了。直接全表扫描计数的方式在分页这种需要快速响应的场景里确实太笨重,用sys.dm_db_partition_stats来获取行数统计绝对是个高效的优化方向。
为什么COUNT(*)这么慢?
当你执行SELECT COUNT(*) FROM MyTable时,SQL Server不得不扫描整个表(或聚集索引的所有叶子节点)来逐行计数,数据量越大,耗时就越夸张。这种全量扫描的操作,完全不符合分页只需要快速拿到总页数的需求。
用dm_db_partition_stats快速获取总行数
这个系统视图存储了SQL Server维护的分区级统计信息,其中就包含了各分区的行数,我们只需要聚合这些数值就能得到总行数,全程不需要扫描表数据,速度快到离谱:
SELECT SUM(rows) AS total_rows FROM sys.dm_db_partition_stats WHERE object_id = OBJECT_ID('MyTable') AND index_id IN (0, 1); -- 0对应堆表,1对应聚集索引
这个查询基本是毫秒级返回,完全能满足分页计算总页数的需求。
处理带过滤条件的分页场景
如果你的分页查询带有WHERE子句(比如WHERE status = 'Active'),直接用上面的语句只能拿到全表总行数。这时候分两种情况处理:
1. 可以接受近似值(大多数分页场景足够)
如果不需要绝对精确的过滤后行数,可以利用对应过滤条件的索引统计信息来估算:
SELECT sp.rows AS approximate_filtered_rows, STATS_DATE(s.object_id, s.stats_id) AS stats_last_updated FROM sys.stats s CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp WHERE s.object_id = OBJECT_ID('MyTable') AND s.name = 'IX_MyTable_Status'; -- 替换为你的过滤字段对应的索引统计名称
只要统计信息是近期更新的,这个近似值会非常接近真实值,而且查询速度依然很快。
2. 需要精确的过滤后行数
如果必须拿到精确值,dm_db_partition_stats就帮不上忙了,但可以通过创建覆盖索引来优化COUNT(*)的速度:
比如你的过滤条件是status = 'Active',可以创建这样的索引:
CREATE NONCLUSTERED INDEX IX_MyTable_Status ON MyTable(status) INCLUDE (id); -- 包含一个主键字段即可,不需要全字段
之后执行SELECT COUNT(*) FROM MyTable WHERE status = 'Active'时,SQL Server会扫描这个窄索引,速度比扫描全表快得多。
注意事项
dm_db_partition_stats里的行数是近似值,如果你刚做了大量的插入/删除操作,可能和实际行数有微小差异。定期执行UPDATE STATISTICS MyTable可以让统计信息更准确。- 如果你的表是分区表,
SUM(rows)会自动聚合所有分区的行数,不需要额外处理。
内容的提问来源于stack exchange,提问作者Thierry




