暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

MySQL 8.0对count(*)的优化

324

count(*)统计技术的操作,不同的数据库可能实现不同,当然性能上也会有一些坑,例如Oracle中,如果索引中存在空值,count(*)还会用全表扫,而不是索引全扫描的访问,因为怕统计错了。

MySQL中的count(*),不同引擎,实现上略有区别,参考技术社群的这篇文章新特新解读 | MySQL 8.0 对  count(*)的优化》,了解下对count(*)所做的优化,知其然,更要知其所以然。

我们知道,MySQL一直依赖对count(*)的执行很头疼。很早的时候,MyISAM引擎自带计数器,可以秒回;但是InnoDB就需要实时计算,所以很头疼。以前有多方法可以变相解决此类问题,例如,
1. 模拟MyISAM的计数器
例如表ytt1,要获得总数,我们建立两个触发器分别对insert/delete来做记录到表ytt1_count,这样只需要查询表ytt1_count就能拿到总数。
ytt1_count这张表足够小,可以长期固化到内存里。不过缺点就是有多余的触发器针对ytt1的每行操作,写性能降低。这里需要权衡。
2. 用MySQL自带的sql_calc_found_rows特性来隐式计算

还是ytt1,不过每次查询的时候用sql_calc_found_rows和found_rows()来获取总数,比如:

  1. mysql> select sql_calc_found_rows * from ytt1 where 1 order by id desc limit 1;

  2. +------+------+

  3. | id | r1 |

  4. +------+------+

  5. | 3072 | 73 |

  6. +------+------+

  7. 1 row in set, 1 warning (0.00 sec)


  8. mysql> show warnings;

  9. +---------+------+-------------------------------------------------------------------------------------------------------------------------+

  10. | Level | Code | Message |

  11. +---------+------+-------------------------------------------------------------------------------------------------------------------------+

  12. | Warning | 1287 | SQL_CALC_FOUND_ROWS is deprecated and will be removed in a future release. Consider using two separate queries instead. |

  13. +---------+------+-------------------------------------------------------------------------------------------------------------------------+

  14. 1 row in set (0.00 sec)


  15. mysql> select found_rows() as 'count(*)';

  16. +----------+

  17. | count(*) |

  18. +----------+

  19. | 3072 |

  20. +----------+

  21. 1 row in set, 1 warning (0.00 sec)

这样的好处是写法简单,用的是MySQL自己的语法。缺点也有,大概有两点,
1. sql_calc_found_rows是全表扫。
2. found_rows()函数是语句级别的存储,有很大的不确定性,所以在MySQL主从架构里,语句级别的行级格式下,从机数据可能会不准确。不过行记录格式改为ROW就OK。所以最大的缺点还是第一点。

从warnings信息看,这种是MySQL 8.0之后要淘汰的语法。

3. 从数据字典里面拿出来粗略的值

  1. mysql> select table_rows from information_schema.tables where table_name = 'ytt1';

  2. +------------+

  3. | TABLE_ROWS |

  4. +------------+

  5. | 3072 |

  6. +------------+

  7. 1 row in set (0.12 sec)

那这样的适合新闻展示,比如行数非常多,每页显示几行,一般后面的很多大家也都不怎么去看。缺点是数据不是精确值。

4. 根据表结构特性特殊的取值

这里假设表ytt1的主键是连续的,并且没有间隙,那么可以直接,
  1. mysql> select max(id) as cnt from ytt1;

  2. +------+

  3. | cnt |

  4. +------+

  5. | 3072 |

  6. +------+

  7. 1 row in set (0.00 sec)

不过这种对表的数据要求比较高。

5. 标准推荐取法(MySQL 8.0.17建议)

MySQL 8.0建议用常规的写法来实现,
  1. mysql> select * from ytt1 where 1 limit 1;

  2. +----+------+

  3. | id | r1 |

  4. +----+------+

  5. | 87 | 1 |

  6. +----+------+

  7. 1 row in set (0.00 sec)


  8. mysql> select count(*) from ytt1;

  9. +----------+

  10. | count(*) |

  11. +----------+

  12. | 3072 |

  13. +----------+

  14. 1 row in set (0.01 sec)

第五种写法是MySQL 8.0.17推荐的,也就是说以后大部分场景直接实时计算就OK了。
MySQL 8.0.17以及在未来的版本都取消了sql_calc_found_rows特性,可以查看第二种方法里的warnings信息。相比MySQL 5.7,8.0对count(*)做了优化,没有必要在用第二种写法了。我们来看看8.0比5.7在此类查询是否真的有优化?
MySQL 5.7,
  1. mysql> select version();

  2. +------------+

  3. | version() |

  4. +------------+

  5. | 5.7.27-log |

  6. +------------+

  7. 1 row in set (0.00 sec)


  8. mysql> explain format=json select count(*) from ytt1\G

  9. *************************** 1. row ***************************

  10. EXPLAIN: {

  11. "query_block": {

  12. "select_id": 1,

  13. "cost_info": {

  14. "query_cost": "622.40"

  15. },

  16. "table": {

  17. "table_name": "ytt1",

  18. "access_type": "index",

  19. "key": "PRIMARY",

  20. "used_key_parts": [

  21. "id"

  22. ],

  23. "key_length": "4",

  24. "rows_examined_per_scan": 3072,

  25. "rows_produced_per_join": 3072,

  26. "filtered": "100.00",

  27. "using_index": true,

  28. "cost_info": {

  29. "read_cost": "8.00",

  30. "eval_cost": "614.40",

  31. "prefix_cost": "622.40",

  32. "data_read_per_join": "48K"

  33. }

  34. }

  35. }

  36. }

  37. 1 row in set, 1 warning (0.00 sec)

MySQL 8.0下执行同样的查询,
  1. mysql> select version();

  2. +-----------+

  3. | version() |

  4. +-----------+

  5. | 8.0.17 |

  6. +-----------+

  7. 1 row in set (0.00 sec)


  8. mysql> explain format=json select count(*) from ytt1\G

  9. *************************** 1. row ***************************

  10. EXPLAIN: {

  11. "query_block": {

  12. "select_id": 1,

  13. "cost_info": {

  14. "query_cost": "309.95"

  15. },

  16. "table": {

  17. "table_name": "ytt1",

  18. "access_type": "index",

  19. "key": "PRIMARY",

  20. "used_key_parts": [

  21. "id"

  22. ],

  23. "key_length": "4",

  24. "rows_examined_per_scan": 3072,

  25. "rows_produced_per_join": 3072,

  26. "filtered": "100.00",

  27. "using_index": true,

  28. "cost_info": {

  29. "read_cost": "2.75",

  30. "eval_cost": "307.20",

  31. "prefix_cost": "309.95",

  32. "data_read_per_join": "48K"

  33. }

  34. }

  35. }

  36. }

  37. 1 row in set, 1 warning (0.00 sec)

从以上结果看出,第二个SQL性能(cost_info)相对第一个提升了一倍。
count操作可以说是数据库中一个很基础的功能,但是它的执行蕴藏着很多数据库的原理,其中一些设计方向是值得我们借鉴学习的。

如果您认为这篇文章有些帮助,还请不吝点下文章末尾的"点赞"和"在看",或者直接转发pyq,




近期更新的文章:
大同两日游攻略和避坑实用帖
tcpdump抓包神器介绍
了解应对三伏天的策略
MySQL的coredump堆栈信息的采集流程
MySQL中timestamp时区转换导致CPU %sy高的场景
校验数据库时间字段合规性

近期的热文:
推荐一篇Oracle RAC Cache Fusion的经典论文
"红警"游戏开源代码带给我们的震撼

文章分类和索引:
公众号1200篇文章分类和索引

文章转载自bisal的个人杂货铺,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论