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

查询处理器


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://xiahunao.cn/news/3269839.html

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

相关文章

C++程序的UI界面闪烁问题的解决办法总结

Windows C++程序复杂的UI界面要使用多种绘图技术(使用GDI、GDI+、ddraw、D3D等绘图),并要贴图去美化,在窗口移动或者改变大小的时候可能会出现闪烁。下面罗列一下UI界面产生闪烁的几种可能的原因,并给出相应的解决办法。 1、原因一 如果熟悉显卡原理的话,调用GDI函数向屏…

JVM系列(三) -类加载器及双亲委派模型介绍

在之前的文章中,介绍了类的加载过程中,我们有提到在加载阶段,通过一个类的全限定名来获取此类的二进制字节流操作,其实类加载器就是用来实现这个操作的。 在虚拟机中,任何一个类,都需要由加载它的类加载器…

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

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

暑假第二周任务——3Gshare的仿写

3GShare的仿写 登陆注册页面 这个界面的UI比较简单,比较困难的地方在于限制我们的输入长度以及我们输入的字符类型。 在这里我是通过在textField的代理中设计限定输入字符的内容,从而实现限制输入长度和限制输入字符的内容,下面给出相关的代…

Day24|二叉树 PART08

235. 二叉搜索树的最近公共祖先 与236类似但可以利用二叉搜索树的性质来做 二叉搜索树:左子 小于头 小于右子 (怪怪的 感觉是不是先记住比较好)(而且也没太理解二叉搜索树的规律) 大体思路:从上到下遍历&a…

html 解决tooltip宽度显示和文本任意位置换行文本显示问题

.el-tooltip__popper {max-width: 480px;white-space: break-spaces; /* 尝试不同的white-space属性值 */word-break:break-all; }

干货:three.js中的六大光源的知识点。

我们在二维屏幕中感知三维场景的一个最重要的要素就是光,光和光源是three.js中一个非常重要的知识点。本文想借着这个话题,为老铁们分享以下六大光源知识点:环境光、点光源、聚光灯、方向光、半球光、平面光。 一、点光源 在 Three.js 中&a…

模拟string(四)详解

目录 判断string大小关系bool operator(const string&s1,const string s2)代码 bool operator<(const string& s1, const string& s2)代码 bool operator<(const string& s1, const string& s2)代码 bool operator>(const string& s1, const …

Stable Diffusion WebUI本地环境搭建

一、项目代码下载 git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui 二、环境配置 conda create --n stafu python3.10.6 实际上跟自己创建的环境没有关系&#xff0c;项目启动会自动复制这个环境&#xff0c;之后项目根据这个基础环境构建 也可以在自己…

机器学习——第一章 绪论

目录 1. 1 引言 1. 2 基本术语 1.3 假设空间 1.4 归纳偏好 1. 1 引言 机器学习致力于研究如何通过计算的手段&#xff0c;利用经验来玫善系统自身的性能在计算机系统中&#xff0c;"经验"通常以"数据"形式存在&#xff0c;因此&#xff0c;机器学习所研…

由字节对齐引发的一场“血案“

最近在搞个网络通信协议&#xff0c; 采用socket udp传输&#xff0c; 运行时&#xff0c;居然报段错误了&#xff0c; 经过debug&#xff0c;发现居然是因为字节对齐问题导致的。 这个问题在实现通信协议&#xff0c;是经常会遇到的问题&#xff0c; 为了方便读者理解&am…

PSVR2下个月将正式支持PC

PlayStation VR 2将于下个月正式支持PC平台。连接PC&#xff0c;需要使用PlayStation VR2头显PC适配器&#xff0c;该适配器将于8月7日发售。 需要注意的是&#xff0c;玩家还需要一根兼容DisplayPort 1.4的线缆、一个Steam账号以及满足最低配置要求的PC。 索尼特别强调&#…

js 替换json中的转义字符 \

例如有以下字符串 "\"{\\\"account\\\":\\\"66\\\",\\\"name\\\":\\\"66\\\"}\"" 想得到如下字符串 {"account":"66","name":"66"} 执行替换字符串 "\"{…

大坝安全监测设备有哪些主要功能?

推荐型号&#xff1a;TH-WY1】大坝安全监测设备的主要功能包括以下几个方面&#xff1a; 1. **实时监测大坝的各项物理参数**&#xff1a;包括应变、位移、水位、流量等<sup>1</sup><sup>2</sup>。 2. **数据处理和分析**&#xff1a;对监测数据进行处…

热门音效、BGM哪里可以免费下载?

剪辑的奇妙世界等你探索&#xff01;在这个创意的领域里&#xff0c;音效是创造氛围、增强表现力的重要元素。我整理了8个优质的剪辑音效素材网站&#xff0c;它们提供了丰富多样的音效资源&#xff0c;无论是制作视频、音乐还是动画&#xff0c;都能为你提供所需的声音。 1、b…

单关节电机动力学辨识

这是一个单关节电机的动力学辨识过程&#xff0c;这是一个yaw轴转动电机的动力学辨识过程 1、动力学建模 &#xff08;1&#xff09;整体动力学 F J α f F J\alpha f FJαf 单关节的物理量包括惯性项、离心力和科氏力、摩擦力。这里忽略离心力和科氏力&#xff0c;据说…

信息学奥赛初赛天天练-47-CSP-J2020完善程序1-质数、因数、质因数、质因数分解算法、质因数分解算法优化

PDF文档公众号回复关键字:20240727 2020 CSP-J 完善程序1 1 完善程序 (单选题 &#xff0c;每小题3分&#xff0c;共30分) 质因数分解给出正整数 n&#xff0c;请输出将 n 质因数分解的结果&#xff0c;结果从小到大输出 例如&#xff1a;输入 n120&#xff0c;程序应该输出…

mysql报错:Unknown collation: ‘utf8mb4_0900_ai_ci‘的原因及解决方法

参考博客&#xff1a;http://t.csdnimg.cn/NRzyk 报错场景描述 使用navicate在查询中运行sql语句时报错&#xff1a;Unknown collation: utf8mb4_0900_ai_ci 报错原因 生成转储文件的数据库版本为8.0&#xff0c;我本地数据库版本为5.6&#xff0c;高版本导入到低版本&…

【C++】透析类和对象(下)

有不懂的可以翻阅我之前文章&#xff01; 个人主页&#xff1a;CSDN_小八哥向前冲 所属专栏&#xff1a;CSDN_C入门 目录 拷贝构造函数 运算符重载 赋值运算符重载 取地址运算符重载 const成员函数 取地址重载 再探构造函数 初始化列表 类型转换 static成员 友元 内…

【CN】Argo 持续集成和交付(一)

1.简介 Argo 英 [ˈɑ:ɡəu] 美 [ˈɑrˌɡo] Kubernetes 原生工具&#xff0c;用于运行工作流程、管理集群以及正确执行 GitOps。 Argo 于 2020 年 3 月 26 日被 CNCF 接受为孵化成熟度级别&#xff0c;然后于 2022 年 12 月 6 日转移到毕业成熟度级别。 argoproj.github.i…