第六章 查询性能优化(下)

查询执行引擎

MySQL只是简单地根据执行计划给出的指令住不住想, 基本上是通过调用存储引擎实现的接口来完成, 这些接口类似搭积木一样能够完成查询的大部分操作.

返回结果给客户端

MySQL将结果返回给客户端是个增量, 逐步返回的过程. 一旦服务器处理完最后一个关联表, 开始生成第一条结果时, MySQL就可以开始向客户端逐步返回结果集了.

MySQL查询优化器的局限性

关联子查询

老版的MySQL子查询实现的非常糟糕, 新版本的MySQL基本没有问题了.
因此最好通过EXPLAIN命令来实际的看效率是否高


image.png

如果效率不高, 改为关联的方式


image.png

如何用好关联子查询

请通过实际测试来看

UNION的限制

如果需要多个表取出数据union后再limit,可以先limit再取出, 可以提高性能.

并行执行

Mysql不支持多核来并行执行查询

哈希关联

老版本MySQL不支持哈希关联,所有的关联都是循环关联,可以通过建立一个哈希索引列模拟哈希关联.

松散索引扫描

Mysql在5.0之后的,松散索引扫描的一些限制通过"索引条件下推"的方式解决

最大值和最小值优化

image.png
mysql> explain select min(actor_id) from actor where first_name='PENELOPE';
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | actor | NULL       | ALL  | NULL          | NULL | NULL    | NULL |  200 |    10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+

LIMIT 1优化后, 当MySQL读到第一个满足条件的记录就停止.


image.png

不过我实验下来没有区别, 可能sql版本不同吧:

mysql> explain select min(actor_id) from actor where first_name='PENELOPE';
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | actor | NULL       | ALL  | NULL          | NULL | NULL    | NULL |  200 |    10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+

mysql> explain select actor_id from actor use index(primary) where first_name='PENELOPE' limit 1;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | actor | NULL       | ALL  | NULL          | NULL | NULL    | NULL |  200 |    10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+

mysql> explain select actor_id from actor where first_name='PENELOPE' limit 1;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | actor | NULL       | ALL  | NULL          | NULL | NULL    | NULL |  200 |    10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+

对同一张表查询和更新

MySQL不允许对同一张表同时进行查询和更新


image.png

可以通过生成临时表来处理, 子查询在UPDATE语句打开表之前已经完成:


image.png

查询优化器的提示(hint)

优化特定类型的查询

count()可以统计某个列值的数量,也可以统计行数
统计某个列时,代表不是NULL 的列
统计行数count(*), * 代表忽略所有的列 直接统计行数,意义清晰,性能会更好

使用近似值

如果count()查询太慢, 可以考虑用explain拿到近似值

更复杂的优化

count() 需要扫描大量的行,优化需要增加汇总表或者类似redis这样的外部缓存系统, 极客时间在第14课讲到了使用redis可能导致不一致问题.

优化关联查询

  • 确保ON或USING子句中的列上有索引, 一般只需要在关联顺序中的第二个表上创建索引, 否则带来额外负担
  • 确保GROUP BY, ORDER BY的表达式只涉及一个表中的类, 这样MySQL才能使用索引做优化
  • 升级MySQL时要仔细评估, 旧的的查询语句可能会变慢甚至结果都发生变化

优化子查询

老版本的MySQL中尽可能用关联查询代替子查询, 不过高版本MySQL已经没有问题了

优化GROUP BY和DISTINCT

首先有限使用索引优化,
如果无法使用索引时,group by 使用两种策略来完成:临时表或文件排序来分组,可以通过 SQL_BIG_RESULT 和 SQL_SMALL_RESULT来让优化器选择希望的方式

若对关联查询分组, 最好使用标识列, 比如下面的语句可以优化为:


优化前

优化后

这个查询在不会有同名的演员的前提下, 改写后的结果不受影响.

  • 不过我发现mysql8.0反而速度降低了
mysql> explain select actor.first_name, actor.last_name, count(*) cnt from film_actor inner join actor using(actor_id) group by actor.first_name, actor.last_name;
mysql> show status like 'Last_query_cost';
+-----------------+------------+
| Variable_name   | Value      |
+-----------------+------------+
| Last_query_cost | 617.732404 |
+-----------------+——————+

mysql> explain select actor.first_name, actor.last_name, c.cnt from actor inner join (select actor_id, count(*) as cnt from film_actor group by actor_id) as c using(actor_id);

mysql> show status like 'Last_query_cost';
+-----------------+-------------+
| Variable_name   | Value       |
+-----------------+-------------+
| Last_query_cost | 2481.148000 |
+-----------------+-------------+

优化limit分页

当一次需要偏移量很大时,尽可能使用索引覆盖扫描, 而不是查询所有的列. 然后根据需要做一次关联操作返回所需的列:

image.png

改写为:
image.png

更详细的, 可以查看我的一篇文章: https://app.yinxiang.com/fx/8e8be2d9-c5df-4cf1-a8f1-602639c4c43b中的分页优化

优化UNION查询

  1. 如果不需要消除重复行, 尽量用UNION ALL.
  2. 将where, limit, order by等子句"下推"到UNION的各个子查询中.

用户自定义变量

略, 有不少技巧, 当工具书查询

案例学习

使用MySQL构建一个队列表

我把我的经验和书中内容结合一下:

  1. 要找到未处理的记录, 一般不会用MySQL的sleep, 而是使用定时job
  2. 要标记正在处理的记录, 不至于让多个消费者重复处理一个记录:


    image.png
    • 书中的方案是不要使用select for update锁表, 而是将该表的owner设置为正在处理这个记录的连接ID:


      image.png

      先更新状态, 再取数据, 根据owner来拿到自己要处理的数据. 若该连接在处理时退出了, 只需要定期运行update语句将其更新为原始状态即可. 可以用show processlist, 获取当前正在工作的线程, 用where条件避免使用到这些刚开始处理的线程:


      image.png
  • 不过我觉得该方案太复杂了, 我们一般可以使用乐观锁, 避免1条记录被多个消费者消费, 在表上加个version字段
  1. 先查询一批待处理的记录
  2. 对每条记录, 使用乐观锁技术更新状态为处理中:
update unset_mails set status='claimed', version=version+1 where id=10 and version=0;
  1. 如果更新成功, 则可以做该任务. 做完后, 将状态翻转为处理成功即可.
  2. 如果更新没成功, 说明有其他消费者同时抢到了该任务, 那就跳过即可.
  3. 最后, 如果消费者在处理时由于某种情况退出, 导致记录一直处于处理中的, 可以定期得将它们翻成待处理即可(一般不会考虑是否有进程仍在处理的情况, 因为业务上可以做个估计, 比如超时了10分钟, 说明肯定是消费进程挂了).
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • MySQL查询优化器的局限性 关联子查询 MySQL的关联子查询实现的很差,最好改成左外连接(LEFT OUTER...
    魅猫之阅读 151评论 0 0
  • 1. 为什么查询速度会慢 如果把查询看作是一个任务,那么它由一系列子任务组成,每个子任务都会消耗一定的时间。如果要...
    李逍遥JK阅读 479评论 1 0
  • 为什么查询速度会慢 查询的生命周期: 客户端->服务器->在服务器上进行解析, 生成执行计划->执行, 并返回结果...
    ThomasYoungK阅读 467评论 0 0
  • 为什么查询会慢? 响应时间、扫描的行数、返回的行数,是衡量查询开销的三个指标(记录在慢日志中) 查询了不需要的数据...
    魅猫之阅读 205评论 0 0
  • 国庆第二天,今天嗨翻模式的开启就等室友的同学来了再说吧,在这之前,先来一波笔记 查询真正重要的是响应时间,查询包含...
    小炼君阅读 1,697评论 0 50