如何编写快速高效的SQL查询(二)——为什么查询速度会慢与优化数据访问样例

news2024/12/26 23:01:14

为什么查询速度会慢?

我们现在已经知道MySQL优化器帮我们做了什么了,在尝试编写快速的查询之前,需要清楚一点,真正重要的是响应时间。如果把查询看作一个任务,那么它由一系列子任务组成,每个子任务都会消耗一定的时间。如果要优化查询,实际上要优化其子任务,要么消除其中一些子任务,要么减少子任务的执行次数,要么让子任务运行得更快。

通常来说,查询的生命周期大致可以按照如下顺序来看:从客户端到服务器,然后在服务器上进行语法解析,生成执行计划,执行,并给客户端返回结果。其中,“执行”可以被认为是整个生命周期中最重要的阶段,这其中包括大量为了检索数据对存储引擎的调用以及调用后的数据处理,包括排序、分组等。在完成这些任务的时候,查询需要在不同的地方花费时间,包括网络、CPU计算、生成统计信息和执行计划、锁等待(互斥等待)等操作,尤其是向底层存储引擎检索数据的调用操作,这些调用需要在内存操作、CPU操作和内存不足时导致的I/O操作上消耗时间。根据存储引擎不同,可能还会产生大量的上下文切换以及系统调用。

在每一个消耗大量时间的查询案例中,我们都能看到一些不必要的操作、某些操作被额外地重复了很多次、某些操作执行得太慢等。优化查询的目的就是减少和消除这些操作所花费的时间。

再次声明一点,对于一个查询的全部生命周期,上面列得并不完整。这里我们只是想说明:了解查询的生命周期和清楚查询的时间消耗情况对于优化查询有很大意义。有了这些概念,我们再一起来看看如何优化查询。

慢查询基础:优化数据访问

一条查询,如果性能很差,最常见的原因是访问的数据太多。某些查询可能不可避免地需要筛选大量数据,但这并不常见。大部分性能低下的查询都可以通过减少访问的数据量的方式进行优化。对于低效的查询,我们发现通过下面两个步骤来分析总是很有效:

  1. 确认应用程序是否在检索大量且不必要的数据。这通常意味着访问了太多的行,但有时候也可能是访问了太多的列。
  2. 确认MySQL服务器层是否在分析大量不需要的数据行。是否向数据库请求了不需要的数据

有些查询会请求超过实际需要的数据,然后这些多余的数据会被应用程序丢弃。这会给MySQL服务器带来额外的负担,并增加网络开销1,另外,这也会消耗应用服务器的CPU和内存资源。

以下是一些典型案例。

查询了不需要的记录

一个常见的错误是,常常会误以为MySQL只会返回需要的数据,实际上MySQL却是先返回全部结果集再进行计算。我们经常会看到一些了解其他数据库系统的人会设计出这类应用程序。这些开发者习惯使用这样的技术,先使用SELECT语句查询大量的结果,然后获取前面的N行后关闭结果集(例如,在新闻网站中取出100条记录,但是只是在页面上显示前面10条)。他们认为MySQL会执行查询,并只返回他们需要的10条数据,然后停止查询。实际情况是,MySQL会查询出全部的结果集,客户端的应用程序会接收全部的结果集数据,然后抛弃其中大部分数据。最简单有效的解决方法就是在这样的查询后面加上LIMIT子句。

多表联接时返回全部列

如果你想查询所有在电影Academy Dinosaur中出现的演员,千万不要按下面的写法编写查询:

这将返回这三个表的全部数据列。正确的方式应该是像下面这样只取需要的列:
在这里插入图片描述

总是取出全部列

每次看到SELECT *的时候都需要用怀疑的眼光审视,是不是真的需要返回全部的列,很可能不是必需的。取出全部列,会让优化器无法完成索引覆盖扫描这类优化,还会为服务器带来额外的I/O、内存和CPU的消耗。因此,一些DBA严格禁止SELECT*的写法,这样做有时候还能避免某些列被修改而带来的问题。

当然,查询返回超过需要的数据也不总是坏事。在我们研究过的许多案例中,人们会告诉我们,这种有点浪费数据库资源的方式可以简化开发,因为能提高相同代码片段的复用性,如果清楚这样做对性能的影响,那么这种做法也是值得考虑的。如果应用程序使用了某种缓存机制,或者有其他考虑,获取超过需要的数据也可能有其好处,但不要忘记这样做的代价是什么。获取并缓存所有的列的查询,相比多个独立的只获取部分列的查询可能更有好处。

重复查询相同的数据

如果你不够小心,很容易出现这样的错误——不断地重复执行相同的查询,然后每次都返回完全相同的数据。例如,在用户评论的地方需要查询用户头像的URL,那么在用户多次评论的时候,可能就会反复查询这个数据。比较好的方案是,当初次查询的时候将这个数据缓存起来,需要的时候从缓存中取出,这样性能显然会更好。

MySQL是否在扫描额外的记录

在确定查询只返回需要的数据以后,接下来应该看看查询为了返回结果是否扫描了过多的数据。对于MySQL,最简单的衡量查询开销的三个指标如下:

  • 响应时间
  • 扫描的行数
  • 返回的行数

没有哪个指标能够完美地衡量查询的开销,但它们大致反映了MySQL在内部执行查询时需要访问多少数据,并可以大概推算出查询运行的时间。这三个指标都会被记录到MySQL的慢日志中,所以检查慢日志记录是找出扫描行数过多的查询的好办法。

响应时间

要记住,响应时间只是一个表面上的值。这样说可能看起来和前面关于响应时间的说法有矛盾,其实并不矛盾,响应时间仍然是最重要的指标,这有一点复杂,后面细细道来。

响应时间是两部分之和:服务时间和排队时间。服务时间是指数据库处理这个查询真正花了多长时间。排队时间是指服务器因为等待某些资源而没有真正执行查询的时间——可能是等I/O操作完成,也可能是等待行锁,等等。遗憾的是,我们无法把响应时间细分到上面这些部分,除非有什么办法能够逐个测量这些消耗,这很难做到。最常见和重要的是I/O等待和锁等待,但是实际情况更加复杂。实际上,I/O等待和锁等待非常重要,因为它们对于性能有着至关重要的影响。

所以在不同类型的应用压力下,响应时间并没有一致的规律或者公式。诸如存储引擎的锁(表锁、行锁)、高并发资源竞争、硬件响应等诸多因素都会影响响应时间。所以,响应时间既可能是一个问题的结果也可能是一个问题的原因,不同案例情况不同。

当你看到一个查询的响应时间时,首先需要问问自己,这个响应时间是否是一个合理的值。实际上可以使用“快速上限估计”法来估算查询的响应时间,这是在Tapio Lahdenmaki和Mike Leach编写的Relational Database Index Design and the Optimizers(Wiley出版社出版)一书中提到的技术,限于篇幅,在这里不会详细展开。概括地说,了解这个查询需要哪些索引以及它的执行计划是什么,然后计算大概需要多少个顺序和随机I/O,再用其乘以在具体硬件条件下一次I/O的消耗时间。最后把这些消耗都加起来,就可以获得一个大概参考值来判断当前响应时间是不是一个合理的值。

扫描的行数和返回的行数

分析查询时,查看该查询扫描的行数是非常有帮助的。这在一定程度上能够说明该查询找到需要的数据的效率高不高。对于找出那些“糟糕”的查询,这个指标可能还不够完美,因为并不是所有行的访问代价都是相同的。较短的行的访问速度更快,内存中的行比磁盘中的行的访问速度要快得多。

理想情况下扫描的行数和返回的行数应该是相同的,但实际中这种“美事”并不多。例如,在做一个联接查询时,服务器必须要扫描多行才能生成结果集中的一行。扫描的行数与返回的行数的比率通常低,一般在1:1到10:1之间,不过有时候这个值也可能非常非常大。

扫描的行数和访问类型

在评估查询开销的时候,需要考虑从表中找到某一行数据的成本。

MySQL有好几种访问方式可以查找并返回一行结果。有些访问方式可能需要扫描很多行才能返回一行结果,也有些访问方式可能无须扫描就能返回结果。

EXPLAIN语句中的type列反映了访问类型。访问类型有很多种,从全表扫描到索引扫描、范围扫描、唯一索引查询、常数引用等。这里列出的这些,速度从慢到快,扫描的行数从多到少。你不需要记住这些访问类型,但需要明白扫描表、扫描索引、范围访问和单值访问的概念。如果你没办法找到合适的访问类型,那么最好的解决办法通常就是增加一个合适的索引,这也是我们下一章将讨论的问题。索引让MySQL以最高效、扫描行数最少的方式找到需要的记录。

例如,我们看一下示例数据库Sakila中的一个查询案例:
在这里插入图片描述
这个查询将返回10行数据,从EXPLAIN的结果可以看到,MySQL在索引idx_fk_film_id上使用了ref访问类型来执行查询:
在这里插入图片描述
EXPLAIN的结果还显示MySQL预估需要访问10行数据。换句话说,查询优化器认为这种访问类型可以高效地完成查询。如果没有合适的索引会怎样呢?MySQL就不得不使用一种糟糕的访问类型,下面来看看如果删除对应的索引再来运行这个查询会发生什么情况:
在这里插入图片描述
正如我们预测的,访问类型变成了一个全表扫描(ALL),现在MySQL预估需要扫描5462条记录来完成这个查询。这里的“Using where”表示MySQL将通过WHERE条件来筛选存储引擎返回的记录。

一般地,MySQL能够使用如下三种方式应用WHERE条件,从好到坏依次为:

  • 在索引中使用WHERE条件来过滤不匹配的记录。这是在存储引擎层完成的。
  • 使用索引覆盖扫描(在Extra列中出现了Using index)来返回记录,直接从索引中过滤不需要的记录并返回命中的结果。这是在MySQL服务器层完成的,但无须再回表查询记录。
  • 从数据表中返回数据,然后过滤不满足条件的记录(在Extra列中出现Using where)。这在MySQL服务器层完成,MySQL需要先从数据表中读出记录然后过滤。

上面这个例子说明了好的索引多么重要。好的索引可以让查询使用合适的访问类型,尽可能地只扫描需要的数据行。但也不是说增加索引就能让扫描的行数等于返回的行数。例如,下面是使用聚合函数COUNT()的查询:
在这里插入图片描述

这条查询语句仅需返回200条记录,但是,它实际读取了多少条记录呢?我们通过前面介绍的EXPLAIN语句来查看一下:
在这里插入图片描述
哇!获取200条记录却需要读取几千行记录,这就意味着,读取了太多不必要的记录。因为WHERE子句中没有过滤掉对应的记录,所以,在这个案例中,索引并不能减少需要扫描的记录行数。

不幸的是,MySQL不会告诉我们生成结果实际上需要扫描多少行数据,而只会告诉我们生成结果时一共扫描了多少行数据。扫描的行中的大部分都很可能是被WHERE条件过滤掉的,对最终的结果集并没有贡献。在上面的例子中,我们删除索引后,看到MySQL需要扫描所有记录然后根据WHERE条件过滤,最终只返回10行结果。理解一个查询需要扫描多少行和实际需要使用的行数需要先去理解这个查询背后的逻辑和思想。

如果发现查询需要扫描大量的数据但只返回少数行,那么通常可以尝试下面的技巧去优化它:

  • 使用索引覆盖扫描,把所有需要用的列都放到索引中,这样存储引擎无须回表获取对应行就可以返回结果了(这是后续需要讨论的问题)。
  • 改变库表结构。例如,使用单独的汇总表。
  • 重写这个复杂的查询,让MySQL优化器能够以更优化的方式执行这个查询(这也是后续需要讨论的问题)。

  1. 如果应用服务器和数据库不在同一台主机上,网络开销就十分明显。即使是在同一台服务器上,也仍然会有数据传输的开销。 ↩︎

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

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

相关文章

Django+mysql+bootstrap学习

python与mysql 创建表结构 create databse unicom DEFAULT CHARSET utf8 COLLATE utf8_general_ci; use unicom; create table admin(id int not null auto_increment primary key,username varchar(16) not null,password varchar(64) not null,mobile char(11) not null )…

Cesium源码分享--量算

Cesium量算插件 在线体验 gitee:https://gitee.com/caozl1132/CesiumExp-measure github:https://github.com/gitgitczl/CesiumExp-measure ps:如果可以的话,希望大家能给我个star,好让我有更新下去的动力&#xff1…

基于SSM的果蔬经营平台系统

末尾获取源码 开发语言:Java Java开发工具:JDK1.8 后端框架:SSM 前端:Vue 数据库:MySQL5.7和Navicat管理工具结合 服务器:Tomcat8.5 开发软件:IDEA / Eclipse 是否Maven项目:是 前言…

Axure教程—动态多散点图(中继器)

本文将教大家如何用AXURE制作动态多散点图 一、效果介绍 如图: 预览地址:https://w8j93u.axshare.com 下载地址:https://download.csdn.net/download/weixin_43516258/87817783 二、功能介绍 简单填写中继器内容即可生成动态多散点图样式…

ChatGpt国内免费网站大全+个人体会

目录 ChatGpt ChatGpt迭代历史 部分ChatGpt个人体会 ChatGpt合集 ChatGpt2步制作流程图与思维导图,你确定不来看一下吗? 当然,你可以自行注册使用ChatGpt(亲测有效) ChatGpt ChatGPT是由OpenAI开发的一种大型语言模型&#…

《The Element of Style》阅读笔记 —— 章节 II Elementary Principles of Composition

前言:本篇为书籍《The Element of Style》第二章的阅读笔记。 本书电子版链接:http://www.jlakes.org/ch/web/The-elements-of-style.pdf 章节 I Elementary Rules of Usage 阅读笔记:链接 Content II Elementary Principles of Composition…

这些脑洞大开的论文标题,也太有创意了O(∩_∩)O

microRNAs啊microRNAs,谁是世界上最致命的髓母细胞瘤microRNAs? 这个标题很容易让人联想到白雪公主后妈说的那句话:Mirror mirror on the wall, who is the fairest of them all? 02 一氧化碳:勇踏NO未至之境 NO 指 nitric oxide…

Spring Boot中使用Spring Batch处理批量任务

Spring Boot中使用Spring Batch处理批量任务 Spring Batch是Spring框架的一个模块,它提供了一组API和工具,用于处理批量任务。在本文中,我们将会介绍如何在Spring Boot中使用Spring Batch来处理批量任务。我们将会使用一个简单的示例来说明如…

在现有iOS项目中,接入新的Flutter项目或现有的Flutter项目

文章参考自Flutter官网:进入Flutter官网 目录 一、背景 二、在现有iOS项目中,接入新的Flutter工程 1、创建新的Flutter工程 2、将iOS工程与Flutter工程进行关联 三、在现有iOS项目中,接入现有的Flutter工程 1、修改Flutter工程中的pub…

如何提高程序员的代码质量?

作为一名程序员,我们需要不断学习、探索新的技术,以便编写出高质量、可维护、安全且高效的代码。但是,即使是经验丰富的程序员也容易遇到一些技术陷阱,这些陷阱可能会导致运行时错误、性能问题或安全漏洞。下面是一些程序员绝对不…

DSOL:一种快速的直接稀疏里程计方案

文章:DSOL: A Fast Direct Sparse Odometry Scheme 作者:Chao Qu, Shreyas S. Shivakumar, Ian D. Miller and Camillo J. Taylor 编辑:点云PCL 代码:https://github.com/versatran01/dsol.git 来源:arXiv2022 欢迎各位…

Elastic 发布 Elasticsearch Relevance Engine™ — 为 AI 革命提供高级搜索能力

作者:Matt Riley 今天我们将向大家介绍 Elasticsearch Relevance Engine™(ESRE™),这是一种创建高度相关的 AI 搜索应用程序的新功能。ESRE 建立在 Elastic 在搜索领域的领导地位以及超过两年的机器学习研究和开发基础之上。Elas…

Java agent入门及demo示例(附源码)

这里是weihubeats,觉得文章不错可以关注公众号小奏技术,文章首发。拒绝营销号,拒绝标题党 背景 继之前我们研究了下skywalking是什么以及skywalking如何监控skywalking 我们并没有探讨过多的skywalking原理 实际上skywalking的实现原理就是java的agent…

Android 12系统源码_窗口管理(一)WindowManagerService的启动流程

前言 WindowManagerService是Android系统中重要的服务,它是WindowManager的管理者,WindowManagerService无论对于应用开发还是Framework开发都是重要的知识点,究其原因是因为WindowManagerService有很多职责,每个职责都会涉及重要…

RabbitMQ发送方确认机制

1、前言 RabbitMQ消息首先发送到交换机,然后通过路由键【routingKey】和【bindingKey】比较从而将消息发送到对应的队列【queue】上。在这个过程有两个地方消息可能会丢失: 消息发送到交换机的过程。消息从交换机发送到队列的过程。 而RabbitMQ提供了…

中国移动董宁:深耕区块链的第八年,我仍期待挑战丨对话MVP

区块链技术对于多数人来说还是“新鲜”的代名词时,董宁已经成为这项技术的老朋友。 董宁2015年进入区块链领域,现任中国移动研究院技术总监、区块链首席专家。作为“老友”,董宁见证了区块链技术多个爆发式增长和平稳发展的阶段,…

基于STC8G1K08A的水压检测系统

基于STC8G1K08A的水压检测系统 前言先来一饱眼福设计和硬件的选型压力传感器选择单片机的选择WIFI透传模块选择 核心代码的开发STC8G1K08A单片机代码读取水压传感器的电压计算对应电压水的压力值猪场水压正常、漏水、喝光水提醒功能的实现 数据通过ESP8266上报到云端代码的实现…

低功耗定时器(LPTIMER)

概述 LPTIM 是运行在Always-On 电源域下的16bits 低功耗定时/计数器模块。通过选择合适的工作时钟,LPTIM 在在各种低功耗模式下保持运行,并且只消耗很低的功耗。LPTIM 甚至可以在没有内部时钟的条件下工作,因此可实现休眠模式下的外部脉冲计数…

新手怎么玩转Linux

Linux是一个非常强大、灵活和可定制的操作系统,这使得它成为了程序员的首选操作系统之一。程序员喜欢使用Linux的原因有以下几点:开源、稳定性、安全性、命令行界面、社区支持。那么新手改如何玩转Linux呢?跟着我一起来看看吧。 以下是对新…

Meta 开源语音 AI 模型支持 1,100 多种语言

自从ChatGPT火爆以来,各种通用的大型模型层出不穷,GPT4、SAM等等,本周一Meta 又开源了新的语音模型MMS,这个模型号称支持4000多种语言,并且发布了支持1100种语言的预训练模型权重,最主要的是这个模型不仅支…