COUNT(*) 查询的优化策略

COUNT(*) 查询的优化策略

COUNT(*) 是常见的”性能杀手”

SELECT COUNT(*) FROM order;  -- 全表扫描 1 亿行

对于 OLTP 系统,这种查询在数据量大时会导致严重的性能问题。

COUNT 的不同写法

-- 四种等价的写法
COUNT(*)      -- 统计行数,包含 NULL 值的行
COUNT(1)      -- 统计行数,等价于 COUNT(*)
COUNT(id)     -- 统计 id 不为 NULL 的行数
COUNT(status) -- 统计 status 不为 NULL 的行数

关键区别
COUNT(*)COUNT(1) 完全等价,都统计所有行(包括所有列为 NULL 的行)
COUNT(column) 只统计该列不为 NULL 的行
– MySQL 8.0 中 COUNT(*) 已经针对 InnoDB 做了优化,性能等于 COUNT(1)

InnoDB 下 COUNT(*) 的性能瓶颈

MyISAM vs InnoDB

-- MyISAM:COUNT(*) 是 O(1) 操作
-- MyISAM 表有单独的行数计数器
SELECT COUNT(*) FROM myisam_table;  -- 瞬间返回

-- InnoDB:COUNT(*) 需要扫描索引
SELECT COUNT(*) FROM innodb_table;  -- 需要扫描索引

InnoDB 不能像 MyISAM 那样缓存行数,因为 InnoDB 支持 MVCC,不同事务看到的行数可能不同。

为什么 InnoDB 的 COUNT(*) 慢

InnoDB 执行 COUNT(*) 的过程:
1. 选择一个最小的二级索引(索引的叶子节点不包含完整行数据,更紧凑)
   1 页 B+Tree 可存 1000 个键值 vs 数据页只能存约 100 行
2. 扫描整个索引页,累加行数
3. 索引越大,扫描越慢

优化策略

策略一:使用近似值

很多业务场景不需要精确的行数:

-- 精确 COUNT
SELECT COUNT(*) FROM order WHERE status = 1;

-- 使用 INFORMATION_SCHEMA 获取近似值
SELECT TABLE_ROWS 
FROM INFORMATION_SCHEMA.TABLES 
WHERE TABLE_NAME = 'order';
-- TABLE_ROWS 是估算值,可能不精确,但非常快

使用场景:后台统计、数据看板、展示”约 X 条结果”。

策略二:使用独立的计数表

-- 创建计数表
CREATE TABLE row_count (
    table_name VARCHAR(100) PRIMARY KEY,
    count BIGINT NOT NULL DEFAULT 0
);

-- 在业务代码中维护计数
-- 插入时 +1
UPDATE row_count SET count = count + 1 WHERE table_name = 'order';
-- 删除时 -1
UPDATE row_count SET count = count - 1 WHERE table_name = 'order';

-- 查询时直接读取
SELECT count FROM row_count WHERE table_name = 'order';

注意:事务中 +1 和插入操作需要在同一个事务中保证一致性。如果并发高,计数表本身会成为热点。

策略三:定时统计缓存到 Redis

// 每 5 分钟统计一次订单总数,缓存到 Redis
@Component
public class CountCacheJob {

    @Scheduled(cron = "0 */5 * * * ?")
    public void refreshOrderCount() {
        long count = orderMapper.count();
        redisTemplate.opsForValue().set("order_count", count);
    }

    // API 接口查询
    public long getOrderCount() {
        Long count = redisTemplate.opsForValue().get("order_count");
        return count != null ? count : 0L;
    }
}

适用于对实时性要求不高的统计场景。

策略四:使用 SHOW TABLE STATUS

SHOW TABLE STATUS LIKE 'order';
-- 输出中 Rows 字段是 InnoDB 的估算值(基于采样)
-- 在数据变化不频繁时,这个估算值也可以接受

策略五:优化带 WHERE 条件的 COUNT

-- 优化 COUNT(*) WHERE column = value
-- 确保 WHERE 条件列上有索引

-- 坏(无索引):
SELECT COUNT(*) FROM order WHERE status = 1;  
-- 全表扫描

-- 好(有索引):
CREATE INDEX idx_status ON order(status);
SELECT COUNT(*) FROM order WHERE status = 1;
-- 只需要扫描 idx_status 索引

-- 更好(覆盖索引):
CREATE INDEX idx_status_created ON order(status, created_at);
SELECT COUNT(*) FROM order WHERE status = 1 AND created_at > '2024-01-01';
-- 直接在索引中完成 COUNT,不用回表

策略六:用数据仓库或 ES 替代

对于复杂的统计分析(如查指定任意时间段的数据量),使用 OLAP 系统或 Elasticsearch:

-- MySQL 负责 OLTP,不承担统计压力
-- 通过 binlog 同步到 ClickHouse 或 ES
-- 统计查询走 ES / ClickHouse

不同数据量级的优化选择

数据量 优化策略
< 100 万 直接 COUNT,不需要优化
100 万 – 1000 万 确保索引正确,执行计划走索引扫描
1000 万 – 1 亿 使用计数表或定时缓存
> 1 亿 数据仓库/OLAP/ES 分担

面试要点

  • InnoDB 的 COUNT(*) 需要扫描索引(区别于 MyISAM 的 O(1))
  • 优化 COUNT 的核心思路是:计数缓存或减小索引扫描范围
  • 使用近似值在很多业务场景下是可以接受的(如”搜索到 X 条结果”)
  • 精确计数在分布式系统中代价极高,能接受最终一致性就最好
  • 理解 COUNT(*) 和 COUNT(column) 的细微区别
© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容