MySQL查询性能优化

1. 为什么查询速度慢

一个查询是由许多子任务组成的,每个子任务都会消耗一定的时间。优化一个查询,其实就是要优化其子任务。

  • 一个查询的生命周期步骤:

    1. 客户端发送一条查询给MySQL服务器

    2. MySQL服务器先检查查询缓存

      • 如果命中缓存,则立刻返回存储在查询缓存中的结果给客户端
      • 否则,进行下一阶段
    3. 解析器进行SQL解析,生成解析树

    4. 预处理器验证权限,确认合法的解析树

    5. 查询优化器根据解析树进行优化,生成对应的查询执行计划

    6. 查询执行引擎根据查询优化器生成的查询执行计划,调用存储引擎的API接口执行查询

    7. 存储引擎数据文件中查询相应数据返回给查询执行引擎

    8. 查询执行引擎存储引擎的API接口返回的结果进行处理

      • 将结果存入查询缓存

      • 将结果返回给客户端

  • 查询花费时间的地方包括:

    • 网络

    • CPU计算

    • 生成统计信息

    • 生成执行计划

    • 锁等待(互斥等待)

    • 向底层存储引擎检索数据的调用操作

      • 内存操作
      • CPU操作
      • 内存不足时导致的I/O操作
    • 上下文切换

    • 系统调用

  • 遇到的问题可能包括:
    • 不必要的额外操作
    • 某些操作被额外地重复了很多次
    • 某些操作执行的太慢
  • 优化查询子任务的思路:
    • 消除其中一些子任务
    • 减少子任务的执行次数
    • 让子任务运行更快

2. 优化数据访问

查询性能低的最基本原因是访问数据太多,可以从两个步骤分析:

  1. 确认应用程序是否在检索大量超过需要的数据

    • 访问了太多的行
    • 访问了太多的列
  2. 确认MySQL服务器是否分析大量超过需要的数据行

1. 是否向数据库请求了不需要的数据

  • 查询不需要的记录

    查询大量结果然后舍弃不需要的。

    解决方法:查询后面加LIMIT。

  • 多表关联时返回全部列

    解决方法:只查询需要的列。

  • 总是取出全部列

    解决方法:若没有缓存则避免使用SELECT *。

  • 重复查询相同的数据

    如用户评论需要查询用户头像URL。

    解决方法:将数据缓存,需要的时候从缓存中获取。

2. MySQL是否在扫描额外的记录

在确定查询只返回需要的数据后,需要查看查询为了返回结果是否扫描了过多的数据。

衡量查询开销的指标:

  • 响应时间

    • 服务时间

    • 排队时间

      • I/O
      • 锁等待(行锁、表锁)

    判断响应时间是否是合理的值(快速上限估计法)

  • 扫描的行数和反回的行数

  • 扫描的行数和返回的类型

    • EXPLAIN语句中的type列反应了访问类型(速度从慢到快、扫描的行数从多到少):

      • 全表扫描
      • 索引扫描
      • 范围扫描
      • 唯一索引查询
      • 常数引用
    • 一般MySQL能够使用如下三种方式应用WHERE条件,从好到坏依次为:

      • 在索引中使用WHERE条件过滤不匹配的记录。(在存储引擎层完成)
      • 使用索引覆盖扫描(在Extra中出现Using index)返回记录,直接从索引中过滤不需要的记录并返回命中的结果。这是在MySQL服务器层完成的,无需再回表查询记录。
      • 从数据表中返回数据,然后过滤不满足条件的记录(在Extra中出现Using where)。这是在MySQL服务器层完成的,MySQL需要先从数据表读取记录然后过滤。
    • 如果发现查询需要扫瞄大量数据但只返回少数的行,通常可以尝试以下技巧:

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

3. 重构查询的方式

  • 将查询转化另一种写法返回相同的结果,但性能更好。

  • 修改应用代码,用另一种方式完成查询。

    • 在设计查询时考虑是否将一个复杂的查询拆分成多个简单的查询。(视情况而定)

    • 切分查询

      • 删除大量数据时会锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但很重要的查询。(一次删除10000行数据比较高效,能减少删除时锁持有时间)
    • 分解关联查询

      可以每次对一个表进行一次单表查询,然后将结果在应用程序中进行关联。

      1. 优势:

        • 让缓存更高效。

          许多应用程序可以方便地缓存单表查询对应的结果对象。对于MySQL的查询缓存,如果关联中的某个表发生变化,那么无法使用查询缓存;拆分后,如果某个表很少改变,那么基于该表的查询就可以重复利用查询缓存结果了。

        • 将查询分解后,执行单个查询可以减少锁的竞争。

        • 在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。

        • 查询本身效率也可能会有所提升。

          使用IN()代替关联查询,可以让MySQL按照ID顺序进行查询,这样可能比随机的关联更高效。

        • 可以减少冗余记录的查询。

          在应用层做关联查询,意味着对某条记录应用之需要查询一次;在数据库中做关联查询,可能需要重复地访问一部分数据。这样的重构可能会减少网络和内存的消耗。

        • 相当于在应用中实现了哈希关联,而不是使用MySQL的嵌套循环关联。(某些场景中哈希关联效率很高)

      2. 场景:

        • 当应用能够方便地缓存单个查询结果的时候。
        • 当可以将数据分布到不同的MySQL服务器上的时候。
        • 当能够使用IN()的方式代替关联查询的时候。
        • 当查询中使用同一个数据表的时候。

4. 查询执行

使用SHOW FULL PROCESSLIST命令查看当前状态:

  • Sleep

    线程正在等待客户端发送新的请求。

  • Query

    线程正在执行查询或者正在将结果发送给客户端。

  • Locked

    在MySQL服务器层,该线程正在等待表锁。

  • Analyzing and statistics

    线程正在收集存储引擎的统计信息,并生成查询的执行计划。

  • Copying to tmp table [on disk]

    线程正在执行查询,并且将其结果都复制到一个临时表中:

    • GROUP BY操作
    • 文件排序操作
    • UNION操作

    on disk标记表示MySQL正在将一个内存临时表放到磁盘上。

  • Sorting result

    线程正在对结果集进行排序。

  • Sending data

    多种情况:

    • 线程可能在多个状态之间传送数据
    • 在生成结果集
    • 在向客户端返回数据

5. 优化特定的查询类型

1. 优化COUNT()查询类型

  • 最好使用COUNT(*)

  • MyISAM在没有任何WHERE条件下使用COUNT(*)才最快

  • 简单的优化:

    • 查找所有ID大于5的城市:

      SELECT COUNT(*) FROM world.city WHERE ID > 5;
      

      通过SHOW STATUS的结果可以看到该查询需要扫瞄大量的行数据。进行优化:

      SELECT (SELECT COUNT(*) FROM world.city) - COUNT(*) FROM world.city WHERE ID <= 5;
      

      这样可以大大减少需要扫描的行数,因为查询优化阶段会将其中的子查询直接当成一个常数来处理。

    • 在同一查询中统计同一个列的不同值得数量:

      SELECT COUNT(color = 'blue' OR NULL) AS blue, COUNT(color = 'red' OR NULL) AS red FROM items;
      
  • 使用近似值

    • 某些业务场景不需要完全精确的COUNT值,此时可以执行EXPLAIN使用近似值来代替。
    • 尝试删除DISTINCT这样的约束来避免文件排序。.
  • 更复杂的查询

    • MySQL层面能做的只有索引覆盖扫描。
    • 可以考虑更改应用架构
    • 可以增加Memcached这样的外部缓存系统。
  • 原则:快速、精确、简单实现,三者永远只能满足其二,必须舍掉其中一个。

2. 优化关联查询

  • 确保ON或者USING子句中的列上有索引。

    在创建索引时就要考虑到关联的顺序。当A表和B表用c列关联时,如果优化器关联顺序是B、A,那么就不需要在B表的对应列上建上索引。没有用到的索引只会带来额外的负担。一般来说,除非有其他理由,否则只需要在关联顺序中的第二个表的相应列上创建索引。

  • 确保任何的GROUP BYORDER BY中的表达式只涉及到一个表中的列。

    这样MySQL才有可能使用索引来优化这个过程。

  • 当升级MySQL的时候需要注意:关联语法、运算符优先级等其他可能会发生变化的地方。

    因为普通关联的地方可能会变成笛卡尔积,不同类型的关联可能会生成不同的结果。

3. 优化子查询

​ 5.6版本以前尽可能使用关联查询。

4. 优化GROUP BYDISTINCT

  • 使用索引是最有效的优化方式。

  • 当无法使用索引时:

    GROUP BY使用两种策略:

    • 使用临时表做分组
    • 使用文件排序做分组

    可以通过使用SQL_BIG_RESULTSQL_SMALL_RESULT来让优化器按照希望的方式运行。

  • 如果需要对关联查询做分组,并且是按照查找表中的某个列进行分组,那么通常采用查找表的标识列分组的效率会比其他列更高。

    下面的查询效率不会很好:

    SELECT actor.first_name, actor.last_name, COUNT(*) 
    FROM sakila.film_actor 
    INNER JOIN sakila.actor 
    USING(actor_id) 
    GROUP BY actor.first_name, actor.last_name;
    

    改写成下面的写法效率会更高:

    SELECT actor.first_name, actor.last_name, COUNT(*) 
    FROM sakila.film_actor 
    INNER JOIN sakila.actor 
    USING(actor_id) 
    GROUP BY film_actor.actor_id;
    

    要注意SQL_MODE可能设置禁止在SELECT中直接使用非分组列,此时会用到子查询,导致创建和填充临时表,而子查询中创建的临时表是没有任何索引的。

    SELECT actor.first_name, actor.last_name, c.cnt 
    FROM sakila.actor 
    INNER JOIN (
      SELECT actor_id, COUNT(*), AS cnt 
      FROM sakila.film_actor 
      GROUP BY actor_id 
    ) AS c USING(actor_id);
    

    当使用GROUP BY时,结果集会自动按照分组字段进行排序。如果不关心排序,可使用GROUP BY NULL让MySQL不进行排序。

5. 优化LIMIT分页

问题:偏移量大时会抛弃前面大量的数据。

解决方法:

  • 在页面中限制分页的数量

  • 优化大偏移量的性能(尽可能使用索引覆盖扫描,而不是查询所有列,然后根据需要关联操作再返回所需的列)

    比如:

    SELECT film_id, description FROM sakila.film ORDER BY title LIMIT 50, 5;
    
    • 通过延迟关联改写为:
    SELECT film_id, description 
    FROM sakila.film 
    INNER JOIN (
      SELECT film_id FROM sakila.film 
      ORDER BY title LIMIT 50, 5
    ) AS lim USING(film_id);    
    
    • 有时也可以将LIMIT查询转换成已知位置查询,让MySQL通过范围扫描获得到对应的结果。如果在一个位置列上有索引,并且预先计算出了边界值,上面的查询就可以改写成:
    SELECT film_id, description 
    FROM sakila.film 
    WHERE position 
    BETWEEN 50 AND 54 
    ORDER BY position;
    
    • 如果可以使用书签记录上次取数据的位置,那么下次就可以直接从该书签记录的位置开始扫描,这样就可以避免使用OFFSET

      首先获取第一组结果:

      SELECT * FROM sakila.rental 
      ORDER BY rental_id DESC LIMIT 20;
      

      假设上面查询返回的主键为10029到10010,那后面的查询就可以从10010开始:

      SELECT * FROM sakila.rental 
      WHERE rental_id < 10010 
      ORDER BY rental_id DESC LIMIT 20;
      

      用这种方式无论翻页到多么后面性能都会非常好。

    • 使用预先计算的汇总表

    • 关联到一个冗余表,冗余表只包含主键列和需要做排序的数据列。

6. 优化UNION查询

MySQL总是通过创建并填充临时表的方式来执行UNION查询。

  • 需要手动将WHERELIMITORDER BY等子句写到UNION的各个子查询中,以便优化器可以充分利用这些条件进行优化。

  • 除非确实需要服务器消除重复的行,否则就一定要使用UNION ALL。如果没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这回导致整个临时表的数据做唯一性检查,代价非常高。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • 玮莲 春天呀,就是这里的草绿了,那里的花开了,人们追逐着花颜,四处熙攘着,吵闹着,而花无论大的小的,俏的艳的,高贵...
    骄阳下的一朵莲阅读 363评论 4 5
  • 不知是从什么时候开始,我很难坚持做一件事,更觉得时间不够用,计划很多,实行下来的很少。尤其是前几年生完孩子,那种没...
    晓佳Judy阅读 360评论 1 8
  • 我叫**,笔名十三叔。其实今年也不过28岁,为什么给自己取这么显老的笔名,因为很喜欢《古惑仔系列》里面由吴君如饰演...
    96160bdff7cf阅读 312评论 1 3
  • 小贺吖阅读 266评论 0 0
  • 菊wfj 姓名:魏凤菊 辽阳市丛迪服装有限公司 六项精进354期学员 六项精进356期志工 六项精进421期志工...
    菊wfj阅读 392评论 0 0