先说个有意思的现象:我面试候选人的时候经常问一个问题——“COUNT(*)、COUNT(1)、COUNT(字段)到底有什么区别?”结果十个人里有七八个会卡壳,剩下两三个能答出来,但追问一句“为什么COUNT(*)在InnoDB里这么慢”就又愣住了。COUNT函数是MySQL里最常用的聚合函数之一,但也是被误解得最深的一个。哪怕是写了好几年SQL的开发者,对它的理解也往往停留在“数行数”这个层面,一旦碰到性能问题、NULL值陷阱、GROUP BY组合场景,就开始凭感觉写,写完也不验证,最后线上出问题才回头排查。
这篇文章我想把MySQL里COUNT函数的底层逻辑、各种写法的真实差异、常见业务场景下的优化手段,以及我在实际项目中踩过的坑一次性讲清楚。不管你是刚接触数据库的新手,还是写了不少年SQL的老手,这篇都值得花几分钟认真过一遍——尤其是那些“你以为你懂,其实理解有偏差”的细节。
1. COUNT的语义本质:它到底在数什么
很多开发者在写COUNT的时候,从来没认真想过一个问题:这行SQL执行完,返回的那个数字,到底是怎么数出来的?是数了整张表的所有行?还是只数了某个字段?NULL值算不算?重复值算不算?理解COUNT的语义,是后续所有性能优化和排错的基础。
1.1 COUNT(*)、COUNT(1)、COUNT(字段)三者的核心区别
先说结论,然后逐个拆解:
| 写法 | 计数规则 | 是否计入NULL值 | 是否计入重复值 | 性能表现 |
|---|---|---|---|---|
COUNT(*) |
统计结果集的行数 | 计入 | 计入 | 最快 |
COUNT(1) |
统计结果集的行数 | 计入 | 计入 | 和COUNT(*)几乎无差异 |
COUNT(字段) |
统计该字段非NULL值的个数 | 不计入 | 计入 | 可能比前两者慢 |
COUNT(DISTINCT 字段) |
统计该字段去重后的非NULL值个数 | 不计入 | 不计入 | 最慢 |
COUNT(*)这个写法,*表示的并不是“所有列”,而是“这一行”。它的语义是:只要这一行存在,就计数加一。所以COUNT(*)统计的是表中所有行的数量,无论某一列的值是不是NULL,这一行都会被算进去。
COUNT(1)里的1是一个常量表达式,对每一行来说,这个表达式的计算结果都是1,永远不为NULL,所以它统计的同样是所有行的数量。从语义上讲,COUNT(1)和COUNT(*)完全等价。
理解了前两个,COUNT(字段)的差异就清楚了:它统计的是该字段的非NULL值个数。如果某一行的这个字段是NULL,这一行就不会被计入。这是SQL标准中聚合函数对NULL的统一处理规则——除了COUNT(*)外,所有COUNT(字段)和COUNT(表达式)都会自动忽略NULL值。
1.2 一个容易踩的NULL值陷阱
这个陷阱我见过太多次了。比如一张订单表,有个字段叫pay_time,表示订单支付时间,没支付的订单这个字段是NULL。业务方想要统计“已支付的订单数量”,写了:
sql复制SELECT COUNT(pay_time) FROM orders;
这个写法本身没错,如果理解了COUNT(字段)忽略NULL的规则,COUNT(pay_time)确实统计的就是支付时间不为空的订单数,也就是已支付订单数。
但问题在于,有些开发者会想当然地认为COUNT(pay_time)和COUNT(*)一样是数行数,看到结果比实际行数少,就开始怀疑数据有问题。更常见的是反过来——想要统计所有订单数的时候,随手写了COUNT(pay_time),结果因为有些订单还没支付,统计出来的数量和期望不一致,线上数据报表出现偏差。
这类问题排查起来非常隐蔽,因为SQL不会报错,结果看起来也“合理”,只有和业务对账的时候才会发现数字对不上。
1.3 COUNT(1)和COUNT(*)在MySQL里的真实性能差异
关于COUNT(1)和COUNT(*)哪个更快,不同数据库产品有不同的处理逻辑。在Oracle里,COUNT(*)经过优化器处理后会等价于COUNT(1);在SQL Server里,两者也基本没有区别。
但在MySQL里,情况有点特殊。在MySQL 5.7及更早版本中,COUNT(*)经过了优化器的特殊处理,在InnoDB引擎下执行时,会选择代价最小的索引进行扫描,性能反而比COUNT(1)更好。在MySQL 8.0中,优化器对两者的处理已经完全等价,性能上没有区别。
所以,网上那些“COUNT(1)比COUNT(*)快”的说法在MySQL里是站不住脚的。我更推荐统一写COUNT(*),理由有两点:一是语义最清晰,不管你面对的表结构是什么样,COUNT(*)永远表示“数所有行”,不容易产生歧义;二是MySQL优化器确实对COUNT(*)做了针对性优化,至少不会比COUNT(1)慢。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. InnoDB引擎下COUNT(*)的性能瓶颈:为什么60万行的表能数出1秒
很多新手第一次遇到COUNT性能问题时,都会产生一个巨大的疑惑:一张表只有几十万行数据,为什么SELECT COUNT(*)要花一两百毫秒甚至更久?几十万行而已,全表扫描也不至于这么慢吧?
这个疑惑背后,是MyISAM和InnoDB两种存储引擎的底层实现差异。理解了这一点,你就理解了MySQL中COUNT性能优化的一半。
2.1 MyISAM为什么快,InnoDB为什么慢
MyISAM引擎的COUNT(*)是出了名的快,原因在于它把每张表的总行数直接保存在表的元数据里。当你执行SELECT COUNT(*) FROM table时,MyISAM不需要去扫描数据文件,直接从元数据里把存好的数值取出来返回。所以不管表里有100行还是1亿行,MyISAM的COUNT(*)响应时间都是毫秒级。
但MyISAM的这种“快”是有代价的:它不支持事务,没有行级锁,崩溃恢复能力差。所以现在主流业务系统早就全面转向InnoDB了。
InnoDB为什么做不到像MyISAM那样直接存一个行数?因为InnoDB支持事务,而事务的核心特性之一就是多版本并发控制(MVCC)。同一个时刻,不同事务可能看到不同版本的数据——A事务还没提交的插入行,对B事务是不可见的。如果InnoDB在元数据里直接存一个“总行数”,那这个数字到底是哪个事务版本下的行数?根本无法定义。所以InnoDB无法维护一个全局精确的行计数器,执行COUNT(*)时必须实时扫描索引或数据页,逐行计数。这就是InnoDB下COUNT(*)慢的根本原因。
2.2 实测一下:60万行数据,不同写法的耗时对比
为了让大家有个直观感受,我用一张60万行的订单表做了个实测(MySQL 8.0,InnoDB引擎,16核/32G云主机):
| 查询语句 | 平均耗时 |
|---|---|
SELECT COUNT(*) FROM orders |
约220ms |
SELECT COUNT(1) FROM orders |
约215ms |
SELECT COUNT(id) FROM orders |
约230ms |
SELECT COUNT(pay_time) FROM orders |
约520ms |
SELECT COUNT(DISTINCT user_id) FROM orders |
约1.8s |
注意看COUNT(pay_time)和COUNT(DISTINCT user_id)的耗时:COUNT(pay_time)因为需要读取pay_time这一列并判断是否为空,涉及到回表(从二级索引回到主键索引取数据),所以比COUNT(id)慢了一倍不止。而COUNT(DISTINCT user_id)需要先对所有user_id排序再统计去重,耗时最久。
这个测试结果说明了一个关键问题:在InnoDB下,COUNT查询的耗时主要由扫描的数据量和是否需要排序决定,而不是由表本身的“总行数”决定。
2.3 为什么COUNT(字段)可能反而慢到想骂人
上面提到COUNT(pay_time)比COUNT(id)慢,原因是回表。这里展开讲一下回表的概念。
InnoDB的索引结构是B+树,主键索引的叶子节点存放的是整行数据,二级索引(非主键索引)的叶子节点存放的是索引字段的值和主键值。如果你执行SELECT COUNT(pay_time) FROM orders,MySQL优先选择使用pay_time字段上的二级索引来扫描,因为二级索引的体量比主键索引小,扫描成本更低。但问题是,二级索引里只存了pay_time的值和主键id,如果要判断pay_time是否为NULL,光看二级索引不够吗?实际上在InnoDB的二级索引中,NULL值也会被记录在索引中,所以判断是否为NULL并不需要回表。
那为什么COUNT(pay_time)还是比COUNT(id)慢呢?关键在于索引的体量。pay_time如果是DATETIME类型,占8字节,加上主键id的8字节,二级索引的每个条目是16字节;而直接扫描主键索引时,每个主键索引条目只有主键的8字节。主键索引的体积更小,扫描的页数更少,IO开销更低。
如果在建表的时候pay_time字段没有加索引,那COUNT(pay_time)就只能走全表扫描,每一行都要读取完整的数据页,性能差距会进一步拉大。
3. 大表COUNT查询的四种优化策略:从执行计划到估算方案
理解了COUNT慢的原因,接下来就要面对一个现实问题:表数据量已经到了几百万、几千万行,业务又需要实时显示总数,怎么办?这里我整理了几种实际项目中常用的优化思路,按推荐程度排序。
3.1 最优方案:用EXPLAIN的估算行数代替精确值
这是最轻量、最高效的方案,但很多人不知道。
MySQL的优化器在执行查询之前,会先通过统计信息和索引分布估算出目标表的大致行数。通过EXPLAIN SELECT * FROM orders,在返回结果的rows字段中就能看到这个估算值。
这个方案的核心逻辑是:**很多业务场景根本不需要精确的行数。**比如后台管理系统的列表页显示“共X条记录”,这个X差几十几百,用户根本感知不到。再比如博客文章列表显示“阅读量X万”,只要量级对了就行。
我常用的做法是写一个估算函数:
python复制def estimate_count(table_name, where_clause=None):
"""
估算表行数,不走COUNT,毫秒级返回
"""
if where_clause:
sql = f"EXPLAIN SELECT * FROM {table_name} WHERE {where_clause}"
else:
sql = f"EXPLAIN SELECT * FROM {table_name}"
cursor.execute(sql)
columns = [col[0] for col in cursor.description]
row = cursor.fetchone()
row_dict = dict(zip(columns, row))
return row_dict['rows']
注意,这个方案有几个前提条件必须满足:
- 表上必须有至少一个索引,否则优化器给不出估算值,只能显示全表扫描的行数(这时估算值等于实际行数,但查询同样很慢)。
- 查询条件中的字段最好有索引,优化器才能基于索引的区分度来估算命中行数。
- 估算值会随着表数据的变化和统计信息的更新而浮动,只适合显示给用户看,不能用于强一致性的对账逻辑。
3.2 使用独立的计数缓存表
如果业务对行数有实时性和精确性的要求,而表本身又大到无法承受每次实时COUNT,那就得从“查询时计算”改成“写入时累加”。
具体做法是创建一张计数表:
sql复制CREATE TABLE orders_count (
total BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
在业务代码里,每次往orders表插入一条数据时,同时执行:
sql复制UPDATE orders_count SET total = total + 1;
删除数据时,执行:
sql复制UPDATE orders_count SET total = total - 1;
读取总数时,直接查orders_count表:
sql复制SELECT total FROM orders_count;
这个方案的性能极好,无论表有多大,读取总数都是毫秒级。
但要注意两个问题:
第一,**计数缓存表和业务主表之间的数据一致性只能靠事务来保证。**以插入为例,必须先插入主表数据,再更新计数表,并且两步操作必须放在同一个数据库事务里。如果两个操作不在同一个事务中,一旦中途出错,计数表和主表的数据就对不上了。
第二,**删除操作的减一逻辑要特别小心。**如果你的删除不是物理删除,而是逻辑删除(用一个is_deleted字段标记),那计数逻辑就复杂了——主表行数没变,但有效行数变了。这种情况下,建议计数表里同时维护total和active_total两个字段,分别对应物理行数和有效行数,更新逻辑分开处理。
3.3 定期汇总表,离线计算精确值
有些场景对实时性要求不高,但要求数值是精确的,比如每日报表中的订单总量、用户总量等。这类需求最合适的方案是定时任务+汇总表。
具体做法是:每天凌晨跑一次离线任务,把昨天的总量和今天的增量合并,写入一张日报表。业务页面需要展示的时候,直接查日报表就好。
sql复制-- 每日汇总示例
INSERT INTO daily_stats (stat_date, total_orders, total_users)
SELECT
CURRENT_DATE(),
COUNT(*) AS total_orders,
COUNT(DISTINCT user_id) AS total_users
FROM orders
WHERE created_at < CURRENT_DATE() + INTERVAL 1 DAY;
这个方案的好处是,计算发生在业务低峰期,应用侧的查询完全不需要和几千万行的表发生关系。
3.4 使用SQL_CALC_FOUND_ROWS还是分页计数?
很多从别的数据库转到MySQL的开发者,习惯用SQL_CALC_FOUND_ROWS来做分页总数统计。这个语法在MySQL里确实存在,但我不推荐使用。
sql复制-- 不推荐
SELECT SQL_CALC_FOUND_ROWS * FROM orders WHERE user_id = 100 LIMIT 10, 20;
SELECT FOUND_ROWS();
SQL_CALC_FOUND_ROWS的执行逻辑是:在满足LIMIT条件之前,先扫描完所有符合条件的行,把总数记下来,再截取需要的分页数据。这意味着,为了返回一页20条数据,MySQL要先把符合条件的所有行都扫一遍。
如果符合条件的数据量很大,而分页只取一页,这样做的效率非常低。相比之下,加一个COUNT(*)查询,由于COUNT(*)只需要扫描索引而不需要回表读取完整行数据,绝大多数情况下比SQL_CALC_FOUND_ROWS更快。
MySQL官方8.0.17之后的优化器也确实在逐步淘汰SQL_CALC_FOUND_ROWS的优化路径。我的建议是放弃这个语法,规规矩矩写两条SQL:一条查总数,一条查当前页数据。
4. COUNT结合GROUP BY和HAVING的真实业务场景
COUNT函数单用很简单,但一旦和GROUP BY、HAVING、JOIN组合起来,各种隐藏的问题就浮现了。这一节用几个真实业务案例来讲清楚。
4.1 场景一:分组统计,为什么COUNT(*)和COUNT(字段)结果不一致
先看一条SQL:
sql复制SELECT
status,
COUNT(*) AS total_orders,
COUNT(pay_time) AS paid_orders,
COUNT(DISTINCT user_id) AS distinct_users
FROM orders
GROUP BY status;
这条SQL的结果表格里,total_orders、paid_orders、distinct_users三个字段表达的是三个完全不同的维度:
total_orders:每个状态下的订单总行数。paid_orders:每个状态下支付时间不为空的订单数,即已支付订单数。distinct_users:每个状态下去重后的用户数。
在实际项目中,这种“一行SQL查多维统计”的写法非常实用,能减少一次数据库往返。但要注意的是,COUNT(DISTINCT ...)和GROUP BY一起用时,MySQL会先在内存中创建临时表做去重,如果去重结果集很大,临时表会落到磁盘上,性能急剧下降。
解决思路有两个:一是把去重字段的值基数和范围控制住,从业务层传入明确的过滤条件;二是如果分组+去重的结果集不可避免很大,考虑拆分成多条SQL分别统计,避免一条SQL把临时表撑爆。
4.2 场景二:HAVING COUNT(*) > N的隐式代价
HAVING子句配合COUNT实现“筛选出现次数超过N次的数据”是常规操作,比如筛选下单超过5次的用户:
sql复制SELECT user_id, COUNT(*) AS cnt
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 5;
很多开发者忽略了一个问题:GROUP BY和HAVING的执行顺序是,先做分组聚合,再用HAVING过滤分组结果。这意味着,如果orders表有1000万行,GROUP BY user_id会先把这1000万行全部分组计算一遍,再过滤出满足条件的分组。
如果你想优化这条SQL的性能,应该先想清楚:能不能在GROUP BY之前就把范围缩小?比如业务上只需要统计最近30天的数据,那就先加一个WHERE created_at >= NOW() - INTERVAL 30 DAY,把参与分组的数据量砍掉一大截。这也是WHERE和HAVING最本质的区别——WHERE在分组前过滤,HAVING在分组后过滤。
4.3 场景三:多表关联里的COUNT陷阱
JOIN查询中的COUNT是最容易出错的场景之一。举一个经典例子,查每个分类下的商品数量:
sql复制SELECT
c.category_name,
COUNT(p.id) AS product_count
FROM categories c
LEFT JOIN products p ON p.category_id = c.id
GROUP BY c.category_name;
这里用COUNT(p.id)而不是COUNT(*),是有讲究的。因为LEFT JOIN时,如果一个分类下没有商品,products表关联出来的行是NULL填充的,如果用COUNT(*),这个分类会被计入“1个商品”——因为这一行虽然全是NULL,但行本身存在。只有用COUNT(p.id),由于该行p.id为NULL,才会被忽略,正确统计出0个商品。
再进一步,如果products表有软删除机制(is_deleted字段),那还要把过滤条件加上:
sql复制SELECT
c.category_name,
COUNT(p.id) AS product_count
FROM categories c
LEFT JOIN products p ON p.category_id = c.id AND p.is_deleted = 0
GROUP BY c.category_name;
注意这里is_deleted = 0必须放在ON子句里,而不是WHERE子句里。如果放在WHERE里,LEFT JOIN会被隐式转成INNER JOIN,没有商品的分类就会被过滤掉,统计结果直接丢数据。
5. 实战排错:一次线上COUNT慢查询的完整排查链路
理论讲再多,不如一个真实案例来得直接。下面是我在维护一个电商后台时遇到的COUNT慢查询问题,完整的排查过程如下,这也是我在日常工作中总结出的一套排错方法论。
5.1 问题现象:接口响应从300ms恶化到4.5秒
当时的业务场景是:管理后台的订单列表页,页面顶部要显示“当前条件下的订单总数”。
某天上线了一个新功能,订单表的数据量从原来的200万涨到了600万。紧接着就收到报警:订单列表接口的P95响应时间从300ms涨到了4.5秒。
第一时间拉日志定位,发现耗时大户就在这条SQL上:
sql复制SELECT COUNT(*) FROM orders WHERE status = 3;
单独拿出来执行,耗时3.8秒。数据量在涨,这条SQL也在变得越来越慢。
5.2 排查第一步:看EXPLAIN,判断扫描行数和索引命中情况
执行计划如下:
sql复制EXPLAIN SELECT COUNT(*) FROM orders WHERE status = 3;
type是ref,用的索引是idx_status(status字段上的普通索引),key_len是1字节,rows预估约581万行。
关键信息出来了:SQL并没有全表扫描,而是走了idx_status索引,但因为status=3的数据量本身就占了大头,优化器估算需要扫描581万行索引条目才能完成计数。所以即使走了索引,代价依然很高。
5.3 排查第二步:确认status字段的区分度
status=3代表“已完成”状态的订单,而订单表中“已完成”是占比最高的状态,估算有580万行,其他状态的数据量加起来才20万。这种情况下,status字段的区分度非常差,索引扫描几乎等价于全表扫描。
这里就引出了一个常见误区:不是“有索引就是快”,索引的提速效果和字段区分度强相关。区分度低的字段,即使建了索引,优化器也不一定会走,甚至走了也不比全表扫描快多少。
5.4 排查第三步:从业务层面降低扫描量
既然单条SQL无法在索引层面进一步优化,就需要改变计数策略。我当时的做法是,跟产品确认了这个总数的展示需求——并不是要求绝对实时,只要每次打开页面时数字是准确的就够了。
于是把SQL改成按天预先聚合,提前在业务代码中维护一张orders_daily_agg统计表:
sql复制CREATE TABLE orders_daily_agg (
stat_date DATE NOT NULL,
status TINYINT NOT NULL,
cnt BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (stat_date, status)
) ENGINE=InnoDB;
每天的定时任务执行:
sql复制INSERT INTO orders_daily_agg (stat_date, status, cnt)
SELECT CURRENT_DATE(), status, COUNT(*)
FROM orders
WHERE created_at < CURRENT_DATE() + INTERVAL 1 DAY
GROUP BY status;
页面要查总数时,把今天的实时数据和历史汇总数据合并:
sql复制SELECT
(SELECT SUM(cnt) FROM orders_daily_agg WHERE status = 3)
+
(SELECT COUNT(*) FROM orders WHERE status = 3 AND created_at >= CURRENT_DATE());
之所以要保留“今天的实时部分”,是因为定时任务一般是凌晨跑,当天的新增数据还没进汇总表。把今天的数据单独COUNT,因为一天新增的数据量有限,比如几万行,扫描代价完全可以接受。
5.5 排查第四步:验证优化效果
修改上线后,订单列表接口的P95响应时间从4.5秒降到了不到200毫秒。这个优化效果立竿见影,而且随着数据量继续增长,页面查询响应时间几乎不受影响——因为历史汇总部分每次都只查一张很小的聚合表。
这个案例里最关键的一步不是技术上的优化手段,而是第一步——通过EXPLAIN看清楚SQL到底干了什么。很多开发者一遇到COUNT慢就想着“加索引”,但索引不是万能的,低区分度字段上的索引对关联查询的性能优化效果极其有限。
6. COUNT相关的几个进阶问题:性能、语义、顺带避雷
最后再整理几个在实际工作中高频出现的问题,供大家排查参考。
6.1 为什么COUNT(*)不会忽略NULL,但COUNT(字段)会
这背后涉及SQL标准中“谓词的三值逻辑”概念。SQL里存在三种逻辑值:TRUE、FALSE、UNKNOWN。NULL在参与比较运算时,结果不是TRUE也不是FALSE,而是UNKNOWN。
COUNT(*)的语义是“统计行的数量”,它不涉及任何字段值的比较,所以不存在NULL问题,统计所有行。而COUNT(字段)的语义是“统计该字段值的数量”,它隐含了一个谓词判断——该字段的值是否不为NULL。如果值为NULL,这个判断结果是UNKNOWN,该行不参与计数。
理解了这一点,你就不会再把COUNT(*)和COUNT(字段)混用了。
6.2 索引状态下COUNT的执行计划选择
当一个表同时存在多个索引时,MySQL优化器会挑选体积最小的索引来执行COUNT(*)。这是优化器的常见策略,因为索引体积越小,扫描时读取的页数越少,IO开销越低。
所以,如果你发现一张大表的COUNT(*)还是慢,可以检查一下表上是否存在一个体量较大的索引,导致优化器做了错误的选择。人为干预的方法是通过FORCE INDEX强制指定某个更小的索引:
sql复制SELECT COUNT(*) FROM orders FORCE INDEX (idx_status);
但一般来说,除非你对执行计划做过充分验证,不要轻易用FORCE INDEX,因为优化器的判断通常是基于统计信息的最优解,人为干预容易适得其反。
6.3 COUNT用的不当,会拖垮整个库
最后提醒一个问题:COUNT是典型的资源密集型聚合操作,在业务高峰期执行大表COUNT,会占用大量的IO和CPU资源,甚至拖垮整个实例上的其他查询。
你有没有想过,为什么很多大厂的数据库规范里都明确禁止在事务中执行COUNT(*)?因为COUNT在InnoDB下会开启一致性快照读(consistent snapshot read),如果它在事务中长时间运行,快照读会占用大量undo log资源,导致其他事务的读写延迟升高。这是生产环境血泪教训换来的规范。
如果你在维护的线上库遇到COUNT慢查询,先别急着在数据库层面硬扛,优先考虑把统计逻辑挪到业务侧,用3.x里提到的计数缓存表或汇总表方案。
6.4 顺带说一句:COUNT和SUM(1)的区别
最后顺带回答一个评论区经常看到的问题:COUNT(*)和SUM(1)的区别是什么。
COUNT(*)是聚合函数,语义是统计行数;SUM(1)是求和函数,语义是“对每行的常量1求和”。在没有分组的情况下,两者的结果是一样的——都是表的总行数。但含义完全不同:SUM(1)先算出每一行的值(1),再把所有值累加。如果表有0行数据,COUNT(*)返回0,SUM(1)返回NULL。所以业务上用SUM(1)代替COUNT(*)没有任何意义,反而引入了NULL值的风险。
我在实际工作中体会最深的一点是:COUNT函数虽然简单,但它背后牵涉到SQL标准语义、InnoDB存储引擎的MVCC实现、索引选择策略、业务架构设计等多个层面。很多线上问题看似是“SQL慢”,但追溯到根因,往往是前期设计时没有想清楚“这个计数到底要满足什么场景、什么频次、什么精度”。下次你写COUNT的时候,不妨多想一步:这个数字真的需要实时精确吗?存储引擎真的支持我这么数吗?索引真的帮上忙了吗?想清楚这几个问题,你踩的坑会比现在少一大半。
