MySQL的COUNT(*)、COUNT(1)、COUNT(column)到底哪个快?

摘要:从一次技术争论出发,通过性能测试和源码分析,揭秘COUNT()、COUNT(1)、COUNT(column)的真实差异。涵盖InnoDB和MyISAM的不同处理方式、覆盖索引的优化技巧、以及大表COUNT的5种优化方案,让你彻底搞懂"为什么COUNT()最快"、"什么时候用COUNT(column)"等面试高频问题。


💥 翻车现场

周五下午,哈吉米正在优化慢查询,南北绿豆路过看了一眼代码。

-- 哈吉米的SQL
SELECT COUNT(*) FROM order_info WHERE status = 1;

南北绿豆:"你怎么用COUNT(*)?应该用COUNT(1),性能更好!"
哈吉米:"???有区别吗?"
阿西噶阿西(凑过来):"我听说COUNT(主键)最快,你们俩都错了!"
南北绿豆:"不对不对,COUNT(1)是常量,不需要取值,肯定最快!"
哈吉米:"那……COUNT(column)呢?"
阿西噶阿西:"那个最慢,因为要判断NULL!"

三人争论了半小时,谁也说服不了谁。

哈吉米:"别吵了!咱们测试一下不就知道了?"


🤔 先说结论:COUNT(*)最快!

测试结果(100万条数据):

SQL执行时间扫描行数
COUNT(*)0.18秒1000000
COUNT(1)0.18秒1000000
COUNT(id)0.21秒1000000
COUNT(status)0.35秒1000000

结论

  • COUNT(*) 和 COUNT(1) 性能完全一样(MySQL优化器会优化成一样的)
  • ✅ COUNT(主键) 稍慢(需要读主键值)
  • ❌ COUNT(普通列) 最慢(需要判断NULL)

哈吉米:"卧槽,COUNT(*) 居然最快?我一直以为它最慢!"

南北绿豆:"那网上说COUNT(1)快是咋回事?"

阿西噶阿西:"来,我给你们讲讲原理。"


🔍 三种写法的本质区别

COUNT(*) - 统计所有行

定义:统计表的总行数,包括NULL值

SELECT COUNT(*) FROM order_info;
-- 含义:统计表中有多少行记录

MySQL的优化

  • InnoDB会选择最小的索引来扫描(通常是二级索引)
  • 不需要读取数据行,只需要统计索引记录数

COUNT(1) - 统计所有行(常量)

定义:统计表的总行数,包括NULL值

SELECT COUNT(1) FROM order_info;
-- 含义:为每一行记录返回常量1,然后统计

实际执行

  • MySQL优化器会把COUNT(1)优化成COUNT(*)
  • 执行计划完全一样

证明

EXPLAIN SELECT COUNT(*) FROM order_info;
EXPLAIN SELECT COUNT(1) FROM order_info;

-- 两个EXPLAIN结果完全相同
+----+-------------+------------+-------+------+---------+
| id | select_type | table      | type  | key  | rows    |
+----+-------------+------------+-------+------+---------+
|  1 | SIMPLE      | order_info | index | idx  | 1000000 |
+----+-------------+------------+-------+------+---------+

南北绿豆:"所以COUNT(1)和COUNT(*)完全一样?"

阿西噶阿西:"对!MySQL优化器会把它们优化成同样的执行计划。"


COUNT(column) - 统计非NULL行

定义:统计某一列非NULL的行数。

SELECT COUNT(status) FROM order_info;
-- 含义:统计status列不为NULL的行数

执行过程

  1. 扫描索引或表
  2. 读取status列的值
  3. 判断是否为NULL
  4. 如果不是NULL,计数+1

为什么慢?

  • 需要读取列的值(IO开销)
  • 需要判断NULL(CPU开销)

示例

-- 表数据
id | status
---|-------
1  | 1
2  | 2
3  | NULL
4  | 1
5  | NULL

-- 查询结果
SELECT COUNT(*);       -- 5(所有行)
SELECT COUNT(1);       -- 5(所有行)
SELECT COUNT(status);  -- 3(非NULL行)

📊 InnoDB vs MyISAM:COUNT(*)的不同处理

哈吉米:"为什么有时候COUNT(*)很快,有时候很慢?"

南北绿豆:"因为不同存储引擎的实现不同!"

MyISAM的COUNT(*)(秒级)

-- MyISAM表
SELECT COUNT(*) FROM myisam_table;
-- 执行时间:0.001秒(极快)

原理

  • MyISAM会在表的元数据中记录总行数
  • COUNT(*)时直接返回这个值,不扫描表
  • 前提:没有WHERE条件

验证

-- 无WHERE条件(极快)
SELECT COUNT(*) FROM myisam_table;
-- 0.001秒

-- 有WHERE条件(慢)
SELECT COUNT(*) FROM myisam_table WHERE status = 1;
-- 5.2秒(需要扫描全表)

InnoDB的COUNT(*)(需要扫描)

-- InnoDB表
SELECT COUNT(*) FROM order_info;
-- 执行时间:0.18秒(需要扫描)

原理

  • InnoDB 不记录总行数(因为MVCC,不同事务看到的行数不同)
  • 必须扫描索引或表来统计
  • 会选择最小的索引来扫描(减少IO)

为什么不记录总行数?

事务A:
START TRANSACTION;
SELECT COUNT(*) FROM order_info;  -- 结果:100万

事务B(同时进行):
START TRANSACTION;
INSERT INTO order_info VALUES (...);  -- 插入了1条
SELECT COUNT(*) FROM order_info;  -- 结果:100万 + 1

问题:同一时刻,两个事务看到的行数不同(MVCC)
所以InnoDB不能简单记录一个总行数

阿西噶阿西:"这就是为什么InnoDB的COUNT(*)比MyISAM慢的原因!"


🎯 COUNT(*)的优化策略

哈吉米:"那InnoDB的COUNT(*)怎么优化?"

南北绿豆:"有5种方案!"


优化1️⃣:利用覆盖索引

原理:MySQL会选择最小的索引扫描。

示例

-- 表结构
CREATE TABLE order_info (
  id BIGINT PRIMARY KEY,       -- 8字节
  order_no VARCHAR(32),        -- 最多128字节
  user_id BIGINT,              -- 8字节
  amount DECIMAL(10, 2),
  status TINYINT,              -- 1字节
  create_time DATETIME,
  INDEX idx_user_id(user_id),  -- 二级索引:8字节
  INDEX idx_status(status)     -- 二级索引:1字节(最小)
);

-- COUNT(*)时,MySQL会选择idx_status索引
SELECT COUNT(*) FROM order_info;
-- Using index (idx_status)

EXPLAIN验证

EXPLAIN SELECT COUNT(*) FROM order_info\G

*************************** 1. row ***************************
          type: index
           key: idx_status    ← 选择了最小的索引
     key_len: 1
        Extra: Using index    ← 覆盖索引

优化建议

  • 建一个占用空间最小的索引(如TINYINT列)
  • MySQL会自动选择这个索引来COUNT

优化2️⃣:用近似值(EXPLAIN的rows)

原理:如果不需要精确值,可以用EXPLAIN的rows估算。

-- 不执行SQL,只看预估行数
EXPLAIN SELECT * FROM order_info\G

*************************** 1. row ***************************
         rows: 982345    ← 预估行数(误差10%左右)

适用场景

  • 展示"大约XX万条数据"
  • 数据看板(不需要精确)

优点

  • ✅ 极快(不扫描表)
  • ✅ 误差可接受(10%以内)

优化3️⃣:用单独的计数表

原理:维护一个专门的计数表。

表结构

CREATE TABLE count_cache (
  table_name VARCHAR(50) PRIMARY KEY,
  total_count BIGINT,
  update_time DATETIME
);

更新计数

-- 插入订单时,更新计数
START TRANSACTION;

INSERT INTO order_info VALUES (...);

-- 更新计数表(同一个事务)
INSERT INTO count_cache (table_name, total_count, update_time)
VALUES ('order_info', 1, NOW())
ON DUPLICATE KEY UPDATE 
  total_count = total_count + 1,
  update_time = NOW();

COMMIT;

查询计数

-- 极快(主键查询)
SELECT total_count FROM count_cache WHERE table_name = 'order_info';

优点

  • ✅ 查询极快(主键查询)
  • ✅ 精确值

缺点

  • ❌ 维护成本高(每次INSERT/DELETE都要更新)
  • ❌ 分布式场景下可能不一致

优化4️⃣:用Redis缓存

原理:把计数存到Redis,定期同步。

代码

@Service
public class OrderService {
    
    @Autowired
    private RedisTemplate<String, Long> redisTemplate;
    
    @Autowired
    private OrderMapper orderMapper;
    
    private static final String COUNT_KEY = "order:count";
    
    /**
     * 获取订单总数(从Redis)
     */
    public Long getOrderCount() {
        Long count = redisTemplate.opsForValue().get(COUNT_KEY);
        
        if (count == null) {
            // Redis没有,从MySQL查询并缓存
            count = orderMapper.count();
            redisTemplate.opsForValue().set(COUNT_KEY, count, 10, TimeUnit.MINUTES);
        }
        
        return count;
    }
    
    /**
     * 创建订单(更新Redis)
     */
    @Transactional
    public void createOrder(Order order) {
        // 1. 插入MySQL
        orderMapper.insert(order);
        
        // 2. Redis计数+1
        redisTemplate.opsForValue().increment(COUNT_KEY);
    }
}

定期同步

// 定时任务:每小时同步一次
@Scheduled(cron = "0 0 * * * ?")
public void syncCountToRedis() {
    Long count = orderMapper.count();
    redisTemplate.opsForValue().set(COUNT_KEY, count);
}

优点

  • ✅ 查询极快(内存读取)
  • ✅ 降低MySQL压力

缺点

  • ⚠️ 可能不精确(Redis和MySQL有延迟)
  • ⚠️ 需要定期同步

优化5️⃣:用SHOW TABLE STATUS

原理:查看表的统计信息(近似值)。

SHOW TABLE STATUS LIKE 'order_info'\G

*************************** 1. row ***************************
           Name: order_info
         Rows: 982345    ← 近似行数
     Data_length: 52428800

优点

  • ✅ 极快(不扫描表)

缺点

  • ❌ 不精确(误差可能较大)
  • ❌ 只能统计整表,不能加WHERE条件

五种方案对比

方案速度精确度复杂度适用场景
覆盖索引⭐⭐⭐✅ 精确通用方案
EXPLAIN rows⭐⭐⭐⭐⭐⚠️ 近似不需要精确值
计数表⭐⭐⭐⭐⭐✅ 精确⭐⭐⭐实时性要求高
Redis缓存⭐⭐⭐⭐⭐⚠️ 可能延迟⭐⭐高并发场景
SHOW TABLE STATUS⭐⭐⭐⭐⭐❌ 不精确快速估算

🎓 面试高频题

题目1:COUNT(*) 和 COUNT(1) 哪个快?

标准答案

性能完全一样。MySQL优化器会把COUNT(1)优化成COUNT(*),执行计划完全相同。

推荐用COUNT(*),因为:

  • ✅ 符合SQL标准
  • ✅ 语义清晰(统计行数)
  • ✅ 所有数据库都支持

题目2:COUNT(*) 和 COUNT(主键) 哪个快?

标准答案

COUNT(*) 更快

原因:

  • COUNT(*):MySQL会选择最小的索引扫描,不需要读取列值
  • COUNT(主键):需要读取主键值,有额外开销

性能差距:约10-20%


题目3:COUNT(column) 什么时候用?

标准答案

当你需要统计非NULL值的数量时

示例:

-- 统计有手机号的用户数
SELECT COUNT(phone) FROM user;

-- 统计所有用户数
SELECT COUNT(*) FROM user;

注意:COUNT(column)最慢,因为需要读取列值并判断NULL。


题目4:如何优化大表的COUNT(*)?

标准答案

5种方案:

  1. 覆盖索引:建一个最小的索引
  2. 近似值:用EXPLAIN的rows估算
  3. 计数表:维护专门的计数表
  4. Redis缓存:计数存Redis,定期同步
  5. 分表:单表过大时,分表后分别COUNT再求和

推荐方案:Redis缓存(高并发)或计数表(实时性)


🎉 结束语

晚上6点,三人终于测试完了所有方案。

哈吉米:"原来COUNT(*)和COUNT(1)完全一样,以前一直被误导了!"

南北绿豆:"对,而且COUNT(*)语义更清晰,推荐用这个。"

阿西噶阿西:"大表COUNT还是要优化,用Redis缓存最实用。"

哈吉米:"我回去就把代码里的COUNT(1)都改成COUNT(*)!"

南北绿豆:"别急,改不改都行,反正性能一样😂"

哈吉米:"……"


记忆口诀

COUNT*最推荐,语义清晰性能好
COUNT数字优化同,列名统计判NULL
InnoDB要扫描,MyISAM有缓存
大表优化有五招,Redis缓存最实用


希望这篇文章能帮你彻底搞懂COUNT的性能问题!下次面试被问到,你就是最靓的仔!💪