高性能MySQL优化要点整合

本篇文章为《高性能MySQL》一书的读书笔记,主要提取了几点在开发中会用到的高性能的做法与一些MySQL的介绍

1. MySQL架构

MySQL最重要、最与众不同的特性是它的存储引擎架构。这种架构的设计将查询处理及其他系统任务和数据的存储/提取相分离。此时可以根据性能、特性以及其他需求来定制化数据存储的方式(存储引擎)

1.1 MySQL逻辑架构

在这里插入图片描述

服务器逻辑架构图,截取自《高性能MySQL》

  • 中间那层架构为MySQL核心部分,负责查询解析、分析、优化、缓存以及内置函数(日期、时间、数学等),所有跨存储引擎的功能都在这一层实现:存储过程、触发器、视图等
  • 最下面一层存储引擎层负责数据的存储和提取,每个存储引擎都有各自的优势和劣势。服务器通过API与存储引擎进行通信。存储引擎API包含了几十个底层函数,用于执行诸如“开始一个事务”等操作。存储引擎不会去解析SQL,只是简单的响应上层服务器的请求

2. Schema与数据类型优化

应该根据系统将要执行的查询语句来设计schema,需要权衡各种因素。例如反泛式的设计可以加快某些查询,但可能使另一个查询变慢。

2.1 数据类型的选择

2.1.1 整数类型

INT(1) 和 INT(20) 在存储与计算上是相同的,只不过其规定了显示字符的个数

存储整数,有如下类型(包括类型所占的存储空间,间接代表了其值的范围)

  • TINYINT(8 bit)
  • SMALLINT(16 bit)
  • MEDIUMINT(24 bit)
  • INT(32 bit)
  • BIGINT(64 bit)

整数类型有可选的UNSIGNED属性,表示无符号,如无符号则存储的值范围扩大一倍(乘2)

2.1.2 字符串类型

VARCHAR

用于存储可变长字符串,需要多消耗1到2的额外字节来存储字符串(记录长度),节省了存储空间,对性能有帮助,但是如果UPDATE使得行变得比之前更长,InnoDB需要分裂页来使行可以放入页内,造成一定的碎片化

下面情况使用VARCHAR是合适的:

  • 字符串列的最大长度比平均长度大很多,充分发挥了变长字符串的节省能力
  • 列的更新很少,这样碎片化就少了

CHAR

MySQL总是根据定义的字符串长度分配空间。对于固定的列,存储空间也比VARCHAR小1到2个字节(记录长度)

扫描二维码关注公众号,回复: 8830964 查看本文章

下面情况使用CHAR是合适的:

  • MD5值,固定的长度,更少的存储空间
  • 很短的字符串,或者所有值都接近同一个长度
  • 经常变更的值,不易产生碎片

VARCHAR(5) 和 VARCHAR(200) 存储的空间开销是一样的,但更长的列会消耗更多的内存(或磁盘),体现在使用内存临时表(或磁盘临时表)进行排序或操作时。

BLOB和TEXT

为存储很大的数据而设计的字符串数据类型

  • MySQL把这两个当作一个独立的对象处理,如果太大,InnoDB会使用专门的外部存储区域来进行存储,此时每个值在行内就存储1-4个字节的指针
  • 两者的不同是BLOB存储的是二进制数据,没有排序规则和字符集,而TEXT有
  • 不能将这两列全部长度进行索引,进而没有覆盖索引和索引消除的排序

使用枚举代替字符串类型

枚举列把一些不重复的字符串存储成一个预定义的集合,MySQL在存储枚举时非常紧凑,会根据列表值的数量压缩到一到两个字节中。

  • MySQL在内部会将每个值在列表中的位置保存为整数,并且在.frm文件中保存数字-字符串的映射关系(查找表),所以在查找时有一些开销,在其与CHAR/VARCHAR列关联时体现,所以有与字符串类型做关联时最好使用字符串(或者枚举与枚举进行关联,会更加的快)
    • 有时候枚举会大大减少表的大小,所以有时候关联的开销也是值得的
  • 缺点是字符串列表是固定的,添加元素只能在列表末尾添加

2.1.3 日期和时间类型

DATATIME

保存大范围的值,从1001年到9999年,精度为秒,格式为YYYYMMDDHHMMSS的整数,使用8个字节的存储空间

TIMESTAMP

时间戳,保存了从1970年1月1日以来的秒数,与UNIX时间戳相同,只使用4个字节的存储空间,只能表示1970到2038年

2.1.4 位数据类型

少数几种存储类型使用紧凑的位存储数据,底层来讲都是字符串类型

BIT

在InnoDB中,为每个BIT列使用一个足够存储的最小整数类型来存放,所以其在InnoDB中不能节省空间,并且结果有时可能令人费解(存储的二进制查找出来的是字符码对应的字符串,例如57的二进制,查找出来会变为"9"),应该谨慎使用BIT类型,如果存储一个true/false值,可以使用CHAR类型,节省了空间又不会令人费解

SET

通过一系列打包的位集合,可以保存很多个true/false的值,有效利用存储空间,例如一条记录里有一列权限,其中有是否可读,是否可写,是否可删除,一系列的是否,通过指定位的0/1来达到

2.1.5 选择标识符

标识符即为一行数据中唯一标识此行的列,需要确保所有关联表中的此标识符都使用同样的类型,包括UNSIGNED这样的属性

  • 整数类型
    • 通常是标识列的最好选择,它们很快并且可以使用AUTO_INCREMENT
  • ENUM和SET
    • 糟糕的选择,此类型是固化的视图,如果确定表主键只有固定那么几个的话,也可以选择
  • 字符串类型
    • 应该避免使用字符串类型作为标识列,它们很消耗空间,通常比数字类型慢
    • 多加注意“随机”的字符串,如MD5、SHA1、UUID等等
      • 插入时会随机写索引的不同位置,使得INSERT更慢,导致页分裂、磁盘随机访问
      • SELECT更慢,因为逻辑相邻的列分布在物理地址的不同位置

2.2 MySQL的schema设计

2.2.1 范式与反范式

范式化的优点:

  • 更新操作比反范式快,没有冗余数据,只需要修改最少的数据
  • 范式化的表通常更小,可以更好的放入内存,执行操作更快
  • 几乎没有冗余数据代表着更少的DISTINCT或者GROUP BY语句

范式化的缺点:

  • 查询时通常需要关联表,才能拿到一个完整需要的数据信息
  • 有时候不同列在不同的表中,原本这些列只需要利用一次索引就可以完成查询

反范式化的优点:

  • 所有数据都在一张表中,避免了关联操作,如果不需要关联表,最差情况下(全表扫描),当数据量比内存大时可能比关联快得多,因为是顺序IO
  • 在一些查询中,使用一个索引就能得到所有的列,有些查询得益于此变得比关联快得多

反范式化的缺点:

  • 列数据冗余存储,更新数据时可能需要更新多表,更新变慢变复杂
  • 行信息不明确,例如用户表与用户发送的消息表合并在一起,通常是很糟糕的

2.2.2 混用范式化与反范式化

  • 没有绝对的范式和反范式
  • 有时候适当冗余某个列(反范式),可以增加查询效率,可以利用索引避免排序操作,一次索引就可以查出所有数据,但相对更新操作代价就高了,需要同时更新所有有冗余数据的表,需要权衡更新与查询

2.2.3 计数器表

有时候需要保存一个计数,来表示例如文件的下载次数等等,创建一张独立的表存储计数器是一个好主意

  • 同时更新次数时UPDATE会有一个全局的互斥锁,这样计数就是串行执行,在高并发下效率很低,可以预先添加例如100个槽,在计数时在100中随机一个数,在随机数的那一行执行UPDATE,这样就可以增加并行性,要获得结果,需要SUM函数聚合计算全部的100个槽

为了提升读,经常会建一些额外索引,增加冗余列,增加缓存,这些方法都会增加写的负担,开发难度也随即增加

3. 创建高性能的索引

索引是存储引擎用于快速找到记录的一种数据结构

3.1 B-Tree索引

  • 大大减少了服务器需要扫描的数据量
  • 避免排序和临时表
  • 将随机IO变为顺序IO

传说中的”三星索引“,也即为索引的最完美追求,如下所示:

  • 索引将相关的记录放到一起
  • 索引中的数据顺序和查找中的排列顺序一致
  • 索引的列包含了查询需要的全部列

3.2 哈希索引

在MySQL中,只有Memory引擎支持哈希索引。Memory引擎同时也支持B-Tree索引

  • 哈希不是顺序存储,无法用于排序
  • 不支持匹配查找,如(A,B)索引不能用在只有A的查询
  • 只能用在等值比较(=、IN()、<=>)
  • 查找数据非常快

在InnoDB中,若索引列非常长,则可以使用自定义的哈希索引,例如存储URL列,使用其做索引会非常大,如果只是将其哈希,放到一个名为 url_crc 这列,保证哈希之后字符串不大,并且将其作为索引,那么下次就可以使用哈希方式进行查询:

SELECT id FROM url WHERE url=“https://blog.csdn.net/qq_41737716” AND url_crc=‘d23hdi23dh’

这样,利用url_crc的索引+URL的哈希,查询会非常快,同时保证了索引不会变的特别大

3.3 高性能索引策略

只讨论InnoDB的B+Tree索引

3.3.1 前缀索引选择

有时候索引列比较长,这让索引变的大且慢,一个策略是之前提到过的自定义哈希索引,还有就是如下的策略:

  • 选择足够长的索引列保证离散性(比如一列只有男/女,离散性相当差)
    • 计算离散性
      • SELECT COUNT(DISTINCT city)/COUNT(*) FROM city
      • SELECT COUNT(DISTINCT LEFT(city, 3))/COUNT(*) FROM city
      • SELECT COUNT(DISTINCT LEFT(city, 4))/COUNT(*) FROM city
    • 创建前缀索引(前缀7)
      • ALTER TABLE city ADD KEY (city(7))
  • 注意,无法使用前缀索引做order by 和 group by 以及 覆盖索引

3.3.2 选择合适的索引列顺序

(多列索引)好的索引列顺序,可以满足order by 和 group by 以及 distinct等查询需求

  • 当不需要考虑排序和分组时,将选择性(离散性)最高的列放在前面,此时索引的作用只是用于WHERE条件的查找,过滤出足够多的行
    • 使用上面的计算离散性公式来计算
  • 在一些情况下,也需要根据运行频率最高的查询来调整顺序

3.4 聚簇索引

优点:

  • 将相关数据保存在一起,只需读取少数的数据页,如果没有,则一行数据需要一次磁盘I/O
  • 数据访问更快
  • 覆盖索引时可以直接使用叶节点的主键值(二级索引本身加主键值也能达到覆盖索引)

缺点:

  • 插入速度严重依赖插入顺序,主键顺序插入时最快方式,如果不是顺序,最好使用OPTIMIZE TABLE命令重新组织一下表
  • 二级索引可能比想象中要大(因为叶子节点是主键列)
  • 二级索引访问需要两次查找

3.4.1 在InnoDB中按主键顺序插入

最好避免随机的聚簇索引,例如使用UUID来作为聚簇索引则会很糟糕,具体请看以下测试

来源于《高性能MySQL》
在这里插入图片描述

可以看到,使用顺序主键插入与uuid随机主键插入,在100万数据时差别是40多秒,在插入100万数据后,再插入300万数据,差别就相当巨大了,需要做一次 OPTIMIZE TABLE 来重建表并优化页的填充

3.5 使用索引扫描来做排序

索引天生自带顺序,如果能利用索引避免排序,将提升性能,如果没有用到索引,而是外部排序(磁盘或者内存),则EXPLAIN执行计划中EXTRA列将会是filesort

  • order by中的列需要满足索引的最左前缀要求即可

3.6 冗余索引

冗余索引指的是,如果有索引 (A,B),再创建 (A) 索引就是冗余索引,此时(A,B) 索引可以当作A使用。如果是 (B,A) 或 (B) 就不算冗余,值得一提的是 (A, ID) 也是冗余的。

  • 大多数情况下都不需要冗余索引,应该尽量扩展已有索引而不是创建新索引。但也有时候处于性能方面考虑需要冗余索引,因为有时候扩展已有的索引会导致其变的太大,从而影响其他使用该索引的查询性能
  • 冗余索引将导致插入、更新、删除操作速度变慢!尽量避免冗余

3.6.1 索引和锁

索引可以让查询锁定更少的行,使得表锁变为行锁

  • 索引可以在存储引擎层面过滤掉无效的行,返回给服务器层时才会应用WHERE语句,此时才会锁定这一部分数据,所以如果索引能过滤掉只剩一行数据的话,此时就是行锁
  • 如果不能使用索引查找的话可能会很糟糕,MySQL会做全表扫描锁住所有的行,此时就是表锁

3.7 索引设计

下面罗列几个索引设计的要点:

  • 如何建立一个支持多种过滤条件的索引
    • 避免多个范围条件
      • 如果有范围查询要放最后(不超过一个范围查询,如果有两个范围查询的列,索引无法使用到后面的索引列了)
    • 什么放前面
      • 查询频率最高的一般放最前面,不考虑排序或组合情况下,选择性高的也可以往前放
    • 重用同一个索引
      • 如果某列不应用在WHERE子句中,则使用IN补救,例如性别,索引为(性别,姓名),此时搜索名为Jack的人,但是不分男女,就可以使用 where 性别 in (‘m’, ‘f’) and 姓名 = jack,同样可以使用到索引(注意避免不要in太多,每个列都in的话组合情况是相乘的,组合太多影响性能)
  • 有多个范围条件怎么办
    • 需要将某个范围转化一个思路,例如需要范围查找一段年龄范围和最近7天上线过的用户,此时可以将7天内这个范围转化为0(7天没上线)和1(7天内有上线)这个字段,从范围变为等值查找,索引(active, age)就可以使用索引了,但需要在用户上线时更新active这个字段为1
    • 上述方法无法精确查找到底是几天上线,此时不妨考虑其中一个范围条件不加入索引,直接放入where语句,如果这个条件的过滤性不高,即使加入索引帮助也不会太大,所以缺少此索引也不会损失太多性能
  • 优化排序
    • 考虑到查询是否有排序,需要利用索引消除filesort排序,例如需要排序一个注册时间字段,并查找所有女性用户,此时是 where 性别 = 女 order by 注册时间,此时如果索引是(性别,注册时间),将会利用到索引过滤不必要扫描的行,并且利用索引直接完成排序,不需要filesort

如果索引优化这条路行不通,看看是否能够重写优化查询语句

4. 查询性能优化

  • 访问的数据太多
    • 查询性能低下最基本的原因是访问的数据太多,某些查询不可避免地需要筛选大量数据
  • 请求了不需要的记录
    • 多表关联时返回全部列
    • 总是取出所有列(有时)
    • 查询不需要的记录
      • 有时候MySQL会返回全部的结果集,然后再进行计算,这样就会获取N行,实际上只用到前10行数据
    • 重复查询相同的数据
      • 这部分可以通过缓存解决,如果重复获取一些改动不大的数据,这无疑是浪费的
  • 扫描了额外的记录
    • 可以查看扫描的行数与返回的行数来确认
    • 查看访问类型(Explain的type)
      • 增加一个合适的索引,来过滤行,以避免扫描过多额外的记录
    • 一般MySQL能够使用如下方式应用WHERE条件,从好到坏依次为:
      • 在索引中使用WHERE过滤记录(存储引擎层)
      • 使用覆盖索引返回记录,直接从索引中得到记录(服务器层,无需回表)
      • 从表中返回数据,在服务器层过滤条件(在服务器层,Extra中为Using Where),这种情况下有可能会有全表扫描但只需要前10条数据的可能,这样极度浪费性能,体现了一个好的索引的重要性

4.1 重构查询

  • 有时候,将一个复杂查询分解为多个简单查询,可能会提升性能
    • 在目前的网络条件下,多个小查询的网络开销并不大
    • 分解关联查询:将一条多关联查询语句分解为多个单查询语句
      • 缓存效率更高:某些表的数据并不会改动,或是不频繁改动,此时此条查询如果缓存下来,就剩下了对此表的查询
      • 数据可以分布到不同的MySQL服务器上
      • 可以使用 IN 来代替关联查询

4.2 查询缓存

如果表发生变化,和这个表相关的所有缓存数据都将失效,这意味着查询缓存并不适用于更新频繁的表,很多时候都应该默认关闭查询缓存

  • 有不确定数据时不会被缓存:NOW、CURRENT_DATE函数,所以需要将日期提前计算好
  • 打开缓存对读与写都有额外的消耗:
    • 读前:检查是否命中缓存
    • 读后:如果没有缓存过,查询之后会放入缓存
    • 写时:将与对应表的所有缓存都设置失效
      • 如果查询缓存使用了大量的内存,那么失效操作就有可能成为一个问题,失效操作由一个全局锁保护的,所有检测是否命中缓存、失效操作都需要等待这个锁
  • 查询缓存是完全存在内存中的
    • 管理内存的开销
    • 放入缓存,分配几个内存数据块的开销(锁住一段固定空间块,找到合适大小数据块)
      • 多次放入缓存的时候难免会产生很多碎片,太小的空间碎片很难被使用到
    • 失效缓存的开销
  • 在InnoDB引擎中,由于其多版本控制,对表的修改事务在提交之前,此表的缓存都无法应用,这使得大事务对缓存命中率很不友好
  • 缓存碎片、内存不足、数据修改都会造成缓存失效
  • 如果空闲块很多,碎片很少,也没有什么由于内存导致的缓存失效,但是命中率还很低,那么很可能说明查询缓存并没有什么好处
  • 在高并发场景下,查询缓存的表现一般来说都比较差

什么情况下可以使用查询缓存

  • 通过观察打开或者关闭确认是否需要打开缓存
  • 有需要消耗大量资源的查询才适合缓存(复杂查询),且UPDATE、DELETE、INSERT相比查询要少非常多

4.3 查询优化器

查询MySQL计算查询的成本

select * from xxx 之后

SHOW STATUS LIKE ‘Last_query_cost’

表示MySQL的优化器认为需要多少个数据页的随机查找完成查询

下面是一些MySQL优化器会做优化的部分:

  • 重新定义关联表的顺序
    • MySQL会将这些关联表中需要扫描函数最少的那张表作为驱动表进行关联排序,注意有时候这样会用不到索引排序(使用STRAIGHT_JOIN来优化此类查询,大部分情况都不会用到)
  • 优化COUNT、MIN、MAX
    • 最大最小值可以使用索引的最右或最左的数据,如果使用到这一优化,此值将为常量对待,并且Explain中可以看到 “Select tables optimized away”
    • COUNT 在MyISAM引擎中可以得到优化(维护了一个变量存放行数,无需扫描)
  • 覆盖索引
  • 列表 IN 的比较
    • in 查询时,会将其列表中先排序,然后通过二分查找的方式确定值是否在列表中
  • 等等…

关联查询-关联表的顺序

  • 有时候,查询语句的关联顺序会导致索引排序失效,而使用filesort的方式排序
    • MySQL在进行filesort的时候需要使用的临时存储空间可能会比想象的大很多,在排序时对每个排序记录都会分配一个定长空间,这个定长空间必须容纳最长的字符串,例如VARCHAR的最大长度
    • 详情看这一篇笔记,和这一篇笔记
  • 有时候,优化器为了寻找关联表需要扫描的行数,可能需要遍历每个表逐个嵌套循环,如果超过10个表的关联,这部分将非常耗时,此时优化器将使用贪婪搜索的方式查找最优的关联顺序,但我们也可以做一个排序顺序优化,可以提前花大时间计算出最优关联表顺序,然后在语句中加上STRAIGHT_JOIN关键字,减少优化器的工作

4.4 关联子查询

MySQL的子查询实现得非常糟糕,最典型的一类查询是where条件中包含in的子查询:

  • select * from xx where xid in ( select * from yy where yid = 1)

其会把外层表压到子查询中:

  • select * from xx where exists ( select * from yy where yid = 1 and yy.xid = xx.xid)

这样,子查询中的语句就无法提前完成,这条语句需要先全表扫描xx表,然后一条一条与yy表匹配。最好改写成如下方式:

  • select xx.* from xx inner join yy using(xid) where yid = 1

有些情况子查询会比较快,但大部分情况下最好使用关联查询,在一些场景中可能需要测试才可以知道子查询和关联查询哪个更快一些

4.5 UNION的限制

有时,MySQL无法将限制条件从外层下推到内层,例如如下UNION语句:

  • ( select * from a order by name ) UNION ALL ( select * from b order by name ) limit 20

这样会把a表的全部记录和b表的全部记录存在一个临时表中,然后从临时表中取出前20条,需要手动将条件给子查询加上:

  • ( select * from a order by name limit 20 ) UNION ALL ( select * from b order by name limit 20 ) limit 20

这样,临时表就只有40条记录了,如果希望全局有序,需要在外层再加一个order by

4.6 最大值和最小值优化

对于MIN()、MAX() 查询,MySQL的优化做的并不好,例如:

  • select min(id) from a where name = ‘jack’

由于nam没有索引,此时会全表扫描,这很糟糕,通过改写成如下解决:

  • select id from a use index(primary) where name = ‘jack’ limit 1

4.7 查询优化器的提示(hint)

  • STRAIGHT_JOIN
    • 放置在select关键之之后,或者两个关联表之间,前者将所有表的顺序定义为语句中的顺序,后者固定前后两表的关联顺序
  • SQL_CACHE和SQL_NO_CACHE
    • 查询缓存
  • FOR UPDATE 和 LOCK IN SHARE MODE
    • 控制select语句的锁机制,很容易造成锁争用,并行性下降的问题,此条语句都可以转换为UPDATE的形式,应该尽量转换,在下面会讨论到
  • USE INDEX、IGNORE INDEX 和 FORCE INDEX
    • 告诉优化器使用或不使用索引
  • SQL_SMALL_RESULT 和 SQL_BIG_RESULT
    • 只对select语句有效,告诉优化器对group by 或者 distinct查询如何使用临时表及排序
    • SQL_SMALL_RESULT:告诉优化器结果集很小,使用内存中的索引临时表
    • SQL_BIG_RESULT:告诉优化器结果集很大,建议使用磁盘临时表做排序操作

4.8 优化特定类型的查询

4.8.1 COUNT查询

  • 使用下面的查询解决两个条件的COUNT计算

    • select count( color = ‘blue’ or null ) as blue, count( color = ‘red’ or null ) as red from item
  • 使用近似值代表总数

    • 有时候count并不需要太过精确,此时可以利用缓存计算一个近似值
  • 通常来说,count都需要扫描大量的行,很难优化

4.8.2 优化关联查询

  • 确保ON或者USING子句中的列有索引
    • 若A表与B表用列C关联,如果关联顺序是B、A,那么只需要在A表上的列C有索引,B表不需要,多余的索引会带来额外的负担
  • 确保GROUP BY 和 ORDER BY 表达式只涉及一个表中的列,这样MySQL才有可能使用索引优化

4.8.3 优化子查询

尽可能使用关联查询代替(MySQL5.6之后可能会优化)

4.8.4 优化GROUP BY 和 DISTINCT

两者都可以使用索引来优化,当无法使用索引时,GROUP BY 使用两种策略完成:

  1. 使用临时表来做分组
  2. 文件排序来做分组

可以通过上面说过的,SQL_SMALL_RESULT 和 SQL_BIG_RESULT 来让优化器按照你希望的方式运行

  • 最有效的方法是使用索引进行优化

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

    • select a.first_name, a.last_name, count(*)
      from b
      	inner join a using(aid)
      group by a.first_name, a.last_name
      

      如果将其换做以下写法,效率会更高

      select a.first_name, a.last_name, count(*)
      from b
      	inner join a using(aid)
      group by b.aid
      -- 或者 group by a.aid 测试结果是更快的
      

      其利用了姓名的唯一性,将其替换为id,因此改写后结果不受影响,但显然firstName和lastName是非分组列,可以通过MIN()和MAX()函数来绕过限制

      select min(a.first_name), max(a.last_name), ...
      
  • 如果没有通过order by子句显式指定排序列,当查询使用group by子句的时候,结果集会自动按照分组的字段进行排序,如果不关心结果集的顺序,而查询又导致了filesort,可以使用order by null不进行排序,也可以在group by子句中直接使用desc或者asc设定方向

4.8.5 优化LIMIT分页

在分页的时候,如果偏移量非常大,比如 limit 10000, 20 ,这样就需要查询10020条记录,但是只返回20行数据,这样的代价非常高

  • 一个思路是尽可能使用覆盖索引,延迟关联,假设有如下sql语句

    • select film_id, description from film order by titil limit 50, 5,如果表非常大,最好改写成如下形式

      • select film_id, description 
        	from film 
        		inner join (
              select film_id from film
              order by title limit 50,5
            )as lim using(film_id)
        
      • 这里的延迟关联将大大提升查询效率,利用覆盖索引拿到主键,再根据主键查询需要的行

  • 也可以将limit查询转换为已知位置的查询,让MySQL通过范围扫描索引获得结果,例如下列sql语句,在列上有索引,预先计算了边界值

  • select film_id, description
    	from film
    		where position between 50 and 54 order by position
    
  • 如果可以省去offset,也是可以进行优化的。每次翻页都保存上次取数据的位置,下次翻页就可以直接从该记录的位置开始扫描,例如使用主键翻页,需要主键是单调增长的

  • select * from a
    	order by id limit 6
    

    假设上次的查询返回主键为10-15的记录,那么下一页就从15这个点开始

    select * from a
    	where id > 15
    		order by id limit 6
    

    这样的性能是可以很好的

  • 最后一种做法是获取并缓存较多的数据,例如缓存1000条,每次分页都从缓存中获取,如果结果集少于1000条,就显示所有的分页链接,如果大于1000条,大于的分页链接隐藏起来,增加一个查找1000条以后数据的按钮

    • 这个做法,是一次扫多行出来,然后受益于多次查询的策略

4.8.6 优化UNION查询

  • MySQL总是通过创建并填充临时表的方式来执行UNION查询。经常需要手工地将where、limit、order by等子句“下推”到UNION的各个子查询中(上面也提到过了)
  • 除非有需要消除重复的行的需求,否则就一定要使用UNION ALL,如果没有ALL关键字,MySQL会给临时表加上DISTINCT选项,这会导致整个临时表啊的数据做唯一性检查,代价非常高

4.8.7 优化SELECT FOR UPDATE

要尽量避免使用SELECT FOR UPDATE,可以直接使用下面的变换替代SELECT FOR UPDATE

假设场景是使用MySQL构建一个任务队列,表中每一条记录都为一项任务,等待客户端取任务执行,每条任务都有3个状态(state):

  • 0:待执行
  • 1:正在执行
  • 2:执行完毕

还有一列字段表示正在执行的线程名称(thread_id,0为无)

例如在一个事务中,先锁住一条任务数据,然后将其状态变为正在运行状态

begin;
-- 锁住待执行的10个任务
select id from task
	where state = 0 and thread_id = 0
	limit 10 for update;
-- result: 1,2,3
-- 将这些任务更新为正在执行
update task
	set state = 1 and thread_id = '12345'
	where id in(1,2,3);
commit;

改写成下面的写法将会更加高效

-- 将10个待执行的任务直接更新为正在执行
update task
	set state = 1 and thread_id = '12345'
	where state = 0 and thread_id = 0
	limit 10;

-- 查询那些刚刚锁定的要执行的任务
select id from task
	where state = 1 and thread_id = '12345'

第一种写法,锁的时间是一整个事务,例子中即为两条语句的时间,但第二种写法,只锁了update那条语句的时间,但两者效果都是一样的,后者的并发性是更高的

所有的SELECT FOR UPDATE都可以使用类似的方法进行改写

发布了84 篇原创文章 · 获赞 85 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/qq_41737716/article/details/102991079