MySQL查询执行(一):count执行慢

news2024/11/13 10:36:54

查询处理器


MySQL查询处理器是MySQL数据库服务器的组件,它负责执行SQL查询。查询处理器的主要任务是解析查询(把用户提交的SQL查询转换为可以被数据库引擎理解和执行的数据操作指令序列),生成查询计划,然后执行该计划。

SQL语句查询编译的步骤:

1)语法分析,建立查询分析树;

2)生成逻辑计划,将分析树转化为初始查询计划,并优化;(逻辑优化,生成逻辑执行计划)

3)生成物理计划,为逻辑计划中的每个操作符选择实现算法以及执行顺序;(物理优化,生成物理执行计划)

相关图如下:

注1:2)、3)步骤通常被称为查询优化器,包含:RBO、CBO策略。

注2:查询优化依赖元数据,如:关系的大小、属性数量及频率、索引、数据在磁盘的分布等。

在开发系统的时候, 你可能经常需要计算一个表的行数, 比如一个交易系统的所有变更记录总数。 这时候你可能会想, 一条select count(*) from t 语句不就解决了吗?

但是, 你会发现随着系统中记录数越来越多, 这条语句执行得也会越来越慢。 然后你可能就想了, MySQL怎么这么笨啊, 记个总数, 每次要查的时候直接读出来, 不就好了吗。

接下来,我们就来聊聊count(*)语句到底是怎样实现的, 以及MySQL为什么会这么实现。 然后, 我会再和你说说, 如果应用中有这种频繁变更并需要统计表行数的需求, 业务设计上可以怎么做。

count(*)的实现方式


你首先要明确的是, 在不同的MySQL引擎中, count(*)有不同的实现方式。

1)MyISAM引擎把一个表的总行数存在了磁盘上, 因此执行count(*)的时候会直接返回这个数,效率很高。

2)而InnoDB引擎就麻烦了, 它执行count(*)的时候, 需要把数据一行一行地从引擎里面读出来, 然后累积计数。

注:这里讨论的是没有过滤条件的count(*), 如果加了where条件的话, MyISAM表也是不能返回得这么快的。

问1:为什么InnoDB不跟MyISAM一样, 也把数字存起来呢?

答:即使是在同一个时刻的多个查询, 由于多版本并发控制(MVCC) 的原因, InnoDB表“应该返回多少行”也是不确定的。

InnoDB的默认隔离级别是可重复读, 在代码上就是通过多版本并发控制, 也就是MVCC来实现的。 每一行记录都要判断自己是否对这个会话可见, 因此对于count(*)请求来说, InnoDB只好把数据一行一行地读出依次判断, 可见的行才能够用于计算“基于这个查询”的表的总行数。

举例:假设表t中现在有10000条记录, 我们设计了三个用户并行的会话。

  • 会话A先启动事务并查询一次表的总行数;
  • 会话B启动事务, 插入一行记录后,查询表的总行数;
  • 会话C先启动一个单独的语句, 插入一行记录后, 查询表的总行数;

假设从上到下是按照时间顺序执行的, 同一行语句是在同一时刻执行的。

你会看到, 在最后一个时刻, 三个会话A、 B、 C会同时查询表t的总行数, 但拿到的结果却不同。

普通索引树比主键索引树小很多, 对于count(*)这样的操作, 遍历哪个索引树得到的结果逻辑上都是一样的。 因此, MySQL优化器会找到最小的那棵树来遍历。

在保证逻辑正确的前提下, 尽量减少扫描的数据量, 是数据库系统设计的通用法则之一。

问2:如果你用过show table status命令的话, 就会发现这个命令的输出结果里面也有一个TABLE_ROWS用于显示这个表当前有多少行, 这个命令执行挺快的, 那这个TABLE_ROWS能代替count(*)吗?

答:不能。因为TABLE_ROWS值是估算得来的,且官方文档说误差可能达到40%到50%。

总结:

MyISAM表虽然count(*)很快, 但是不支持事务。

show table status命令虽然返回很快, 但是不准确。

InnoDB表直接count(*)会遍历全表, 虽然结果准确, 但会导致性能问题。

问3:count(*)这么慢,我该怎么办?

答:自己数。

自己计数有哪些方法呢?下面对几种常用方法逐一介绍。

用缓存系统保存计数(不推荐)

对于更新很频繁的库来说, 你可能会第一时间想到, 用缓存系统来支持。

你可以用一个Redis服务来保存这个表的总行数。 这个表每被插入一行Redis计数就加1, 每被删除一行Redis计数就减1。 这种方式下, 读和更新操作都很快,。

问1:这种计数方式存在什么问题吗?

答:缓存系统不仅存在丢失更新问题,还存在计数值逻辑上不精确。

先说丢失更新问题,如果刚刚在数据表中插入了一行, Redis中保存的值也加了1, 然后Redis异常重启了, 重启后你要从存储redis数据的地方把这个值读回来, 而刚刚加1的这个计数操作却丢失了。丢失更新解决方案:当Redis异常重启以后, 到数据库里面单独执行一次count(*)获取真实的行数, 再把这个值写回到Redis里就可以了。 异常重启毕竟不是经常出现的情况, 这一次全表扫描的成本, 还是可以接受的。

即使丢失更新问题可以被解决,亦或是Redis正常工作, 但这个值在逻辑上也是不精确的。不精确定义如下:

  • 一种是, 查到的100行结果里面有最新插入记录, 而Redis的计数里还没加1。
  • 另一种是, 查到的100行结果里没有最新插入的记录, 而Redis的计数里已经加了1。

分别对上述两种情况进行举例说明:

1)情况一

上图中,会话A是一个插入交易记录的逻辑, 往数据表里插入一行R, 然后Redis计数加1; 会话B就是查询页面显示时需要的数据。

在上图的这个时序里, 在T3时刻会话B来查询的时候, 会显示出新插入的R这个记录, 但是Redis的计数还没加1。 这时候, 就会出现我们说的数据不一致。

2)情况二

你会发现, 这时候反过来了, 会话B在T3时刻查询的时候, Redis计数加了1了, 但还查不到新插入的R这一行, 也是数据不一致的情况。

在并发系统里面, 我们是无法精确控制不同线程的执行时刻的, 因为存在图中的这种操作序列,所以, 即使Redis正常工作, 这个计数值还是逻辑上不精确的。

注:Redis不支持分布式事务, 无法拿到精确一致的视图。所以Redis不能像MySQL一样使用事务解决计数不精确问题。

在数据库保存计数(推荐)

问:如果我们把这个计数直接放到数据库里单独的一张计数表C中, 又会怎么样呢?

答:不仅能解决崩溃丢失问题(InnoDB支持使用redo log+binlog解决崩溃丢失),还能解决计数不精确问题。

计数不精确解决思路:以子之矛攻子之盾。既然计数不精确是由于InnoDB引擎支持事务导致的,那么就利用事务特性解决该问题。

我们来看下现在的执行结果。 虽然会话B的读操作仍然是在T3执行的, 但是因为这时候更新事务还没有提交, 所以计数值加1这个操作对会话B还不可见。

因此, 会话B看到的结果里, 查计数值和“最近100条记录”看到的结果, 逻辑上就是一致的。

不同的count用法


思考:在select count(?) from t这样的查询语句里面, count(*)、 count(主键id)、 count(字段)和count(1)等不同用法的性能, 有哪些差别?

count()语义:count()是一个聚合函数, 对于返回的结果集, 一行行地判断, 如果count函数的参数不是NULL, 累计值就加1, 否则不加。 最后返回累计值。

  1. count(*)、 count(主键id)和count(1) 都表示返回满足条件的结果集的总行数。
  2. count(字段) , 则表示返回满足条件的数据行里面, 参数“字段”不为NULL的总个数。

至于分析性能差别的时候, 你可以记住这么几个原则:

  1. server层要什么就给什么。
  2. InnoDB只给必要的值。
  3. 现在的优化器只优化了count(*)的语义为“取行数”, 其他“显而易见”的优化并没有做。

这是什么意思呢? 接下来, 我们就一个个地来看看。

  1. count(*)是例外, 并不会把全部字段取出来, 而是专门做了优化, 不取值。 count(*)肯定不是null, 按行累加。(不取值)
  2. 对于count(1)来说, InnoDB引擎遍历整张表, 但不取值。 server层对于返回的每一行, 放一个数字“1”进去, 判断是不可能为空的, 按行累加。单看这两个用法的差别的话, 你能对比出来, count(1)执行得要比count(主键id)快。 因为从引擎返回id会涉及到解析数据行, 以及拷贝字段值的操作。(不取值)
  3. 对于count(主键id)来说, InnoDB引擎会遍历整张表, 把每一行的id值都取出来, 返回给server层。 server层拿到id后, 判断是不可能为空的, 就按行累加。(取值,判断一次)
  4. 对于count(字段)来说:

看到这里, 你一定会说, 优化器就不能自己判断一下吗, 主键id肯定非空啊, 为什么不能按照count(*)来处理, 多么简单的优化啊。

当然, MySQL专门针对这个语句进行优化, 也不是不可以。 但是这种需要专门优化的情况太多了, 而且MySQL已经优化过count(*)了, 你直接使用这种用法就可以了。

所以结论是: 按照效率排序的话, count(字段)

小结:思考题


思考:我们用了事务来确保计数准确。 由于事务可以保证中间结果不被别的事务读到, 因此修改计数值和插入新记录的顺序是不影响逻辑结果的。 但是, 从并发系统性能的角度考虑, 你觉得在这个事务序列里, 应该先插入操作记录, 还是应该先更新计数表呢?

逻辑实现上是启动一个事务, 执行两个语句:

  1. insert into 数据表。
  2. update 计数表, 计数值加1。

从并发系统性能的角度考虑, 应该先插入操作记录, 再更新计数表。

因为更新计数表涉及到行锁的竞争, 先插入再更新能最大程度地减少事务之间的锁等待, 提升并发度。

注:该小节的讨论基于InnoDB引擎。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1952739.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C++】循环即热案例-猜数字

在 1~100 中选取一个随机数,玩家输入数字,判断输入的数字与这个随机数的大小并输出以作为提示,在猜对后提示猜测正确并退出游戏 (若想活得一个随机数需要用到一个 rand() 函数,例如题中要求则要写 rand()%1001 &#…

【QT】QT 窗口(菜单栏、工具栏、状态栏、浮动窗口、对话框)

Qt 窗口是通过 QMainWindow类来实现的。 QMainWindow 是一个为用户提供主窗口程序的类,继承自 QWidget 类,并且提供了⼀个预定义的布局。QMainWindow 包含一个菜单栏(Menu Bar)、多个工具栏(Tool Bars)、…

普乐蛙VR航天航空体验馆知识走廊VR体验带你登陆月球

VR航天航空设备是近年来随着虚拟现实(VR)技术的快速发展而兴起的一种新型设备,它结合了航天航空领域的专业知识与VR技术的沉浸式体验,为用户提供了前所未有的航天航空体验。以下是对VR航天航空设备的详细介绍: 一、设备…

时间复杂度与O(n)

文章目录 1 复杂度分析1.1 时间复杂度1.1.1 循环执行次数1.1.2 大O(n)表示法 1.2 空间复杂度 1 复杂度分析 1.1 时间复杂度 ​ 时间复杂度用来表示算法运行时间的长短,用来定性的描述程序的运行时间。要了解时间复杂度,我们需要先了解程序执行的次数。…

法律 | 法律人AI使用指南

原文:法律 | 法律人AI使用指南|法官|法院|文书|公司法_网易订阅 01 引言 过去半年多,我一直在尝试着用AI来辅助自己的各项法律工作,将AI融入自己的日常工作之中,并试图形成自身稳定的“法律AI”工作流。在此过程中,…

vuex学习day02-state状态、严格模式(strict)、mutations、辅助函数mapMutations、actions

4、state状态 (1)作用:提供共享数据 (2)步骤: 1)找到仓库,通过state提供共享数据 报错1?: 解决方式: 找到.eslintrc.js文件,添加一…

【React】项目的目录结构全面指南

文章目录 一、React 项目的基本目录结构1. node_modules2. public3. src4. App.js5. index.js6. .gitignore7. package.json8. README.md 二、React 项目的高级目录结构1. api2. hooks3. pages4. redux5. utils 三、最佳实践 在开发一个 React 项目时,良好的目录结构…

Flink笔记整理(五)

Flink笔记整理(五) 文章目录 Flink笔记整理(五)七、处理函数(最底层最常用最灵活)7.1基本处理函数(ProcessFunction)处理函数的功能和使用ProcessFunction解析 7.2按键分区处理函数&…

【初阶数据结构】9.二叉树(4)

文章目录 5.二叉树算法题5.1 单值二叉树5.2 相同的树5.3 另一棵树的子树5.4 二叉树遍历5.5 二叉树的构建及遍历 6.二叉树选择题 5.二叉树算法题 5.1 单值二叉树 点击链接做题 代码: /*** Definition for a binary tree node.* struct TreeNode {* int val;* …

鱼哥好书分享活动第27期:看完这篇《云原生安全》了解云原生环境安全攻防实战技巧!

鱼哥好书分享活动第27期:看完这篇《云原生安全》了解云原生安全攻防实战技巧! 主要内容:读者对象:本书目录:了解更多:赠书抽奖规则: 当前全球数字化的发展逐步进入深水区,云计算模式已经广泛应用…

用 apifox cli 命令行运行本地接口出现TypeError:Invalid IP address: undefined

用 apifox cli 命令行运行本地接口出现TypeError:Invalid IP address: undefined,客户端运行是通过的但命令行运行会报错 修改端口也是一样报错,地址修改为127.0.0.1会报错connect ECONNREFUSED 127.0.0.1:8080 解决方法:不用localhost&…

视觉SLAM第一讲

第一讲-预备知识 SLAM是什么? SLAM(Simultaneous Localization and Mapping)是同时定位与地图构建。 它是指搭载特定传感器的主体,在没有环境先验信息的情况下,于运动过程中建立环境的模型,同时估计自己…

《Milvus Cloud向量数据库指南》——Milvus Cloud不同场景下的部署形态选型

不同场景下的部署形态选型 一般说选型肯定离不开阶段。用到向量数据库的应用基本有这么几个阶段: AI 应用的快速原型构建。比如你在做一个 AI 个人助手、一个小的搜索引擎原型、一个端到端的 RAG 原型,这类项目的迭代速度是很关键的,而且原型构建期不需要关心性能或者稳定性…

JVM 内存分析工具 Memory Analyzer Tool(MAT)入门(一)

一、打开 jvisualvm (VisualVM 是一款集成了 JDK 命令行工具和轻量级剖析功能的可视化工具。 设计用于开发和生产。) 打开 jvisualvm.exe 工具会出现如下一些监控指标 二、VisualVM可以根据需要安装不同的插件,每个插件的关注点都不同&#x…

街道宣传信息工作通讯稿怎样向新闻媒体投稿?

在街道单位从事信息宣传工作的初期,我深刻体会到了这份工作的艰辛与挑战。面对繁重的投稿任务和严苛的审核机制,传统的邮箱投稿方式如同一座难以逾越的大山,横亘在我与成功之间。每一篇精心撰写的通讯稿,都承载着对单位工作的热情与期待,却在漫长的等待与频繁的退稿中逐渐消磨了…

Java实现七大排序(二)

一.交换排序 1.冒泡排序 这个太经典了&#xff0c;每个学编程都绕不开的。原理跟选择排序差不多&#xff0c;不过冒泡排序是直接交换。 public static void bubbleSort(int[] array){for (int i 0; i < array.length - 1; i) {for (int j 0; j < array.length-1-i; j…

助力运动员突破数据障碍 英特尔助力巴黎奥运会构建RAG聊天机器人

随着现代科技的飞速发展&#xff0c;奥运会这样的大型体育赛事也迎来了前所未有的变革。从运动员训练到比赛直播&#xff0c;从裁判判罚到观众体验&#xff0c;科技的应用正深刻地影响着体育赛事的每一个环节。近日&#xff0c;英特尔就分享了与国际奥林匹克委员会&#xff08;…

Docker快速搭建WordPress博客系统网站

WordPress 是一款广泛使用的开源内容管理系统(CMS),用于创建和管理网站和博客。 主要功能: 易于使用的界面:WordPress 提供了一个直观的后台管理界面,使用户能够轻松创建、编辑和管理网站内容。 主题和模板:WordPress 提供了各种主题和模板,可根据网站需求进行选择和自…

OceanBase v4.2 特性解析:如何实现表级恢复

背景 在某些情况下&#xff0c;你可能会因为误操作而遇到表数据损坏或误删表的情况。为了能在事后将表数据恢复到某个特定时间点&#xff0c;在OceanBase尚未有表级恢复功能之前&#xff0c;你需要进行以下步骤&#xff1a; 利用OceanBase提供的物理恢复工具&#xff0c;您可…

进程概念(三)----- fork 初识

目录 前言1. pid && ppid2. forka. 为什么 fork 要给子进程返回 0&#xff0c; 给父进程返回子进程的 pid &#xff1f;b. 一个函数是如何做到两次的&#xff1f;c. fork 函数在干什么&#xff1f;d. 一个变量怎么做到拥有不同的内容的&#xff1f;e. 拓展&#xff1a;…