背景

早上收到某系统的告警tidb节点挂掉无法访问,情况十万火急。登录中控机查了一下display信息,4个TiDB、Prometheus、Grafana全挂了,某台机器hang死无法连接,经过快速重启后集群恢复,经排查后是昨天上线的某个SQL导致频繁OOM。

于是开始亡羊补牢,来一波近期慢SQL巡检 #手动狗头#。。。

随便找了一个出现频率比较高的慢SQL,经过优化后竟然性能提升了1500倍以上,感觉有点东西,分享给大家。

分析过程

该慢SQL逻辑非常简单,就是一个单表聚合查询,但是耗时达到8s以上,必有蹊跷。

脱敏后的SQL如下:

SELECT
cast( cast( CAST( SUM( num ) / COUNT( time ) AS CHAR ) AS DECIMAL ( 9, 2 )) AS signed ) speed,
... -- 此处省略n个字段
FROM
(
SELECT
DATE_FORMAT( receive_time, '%Y-%m-%d %H:%i:00' ) AS time,
COUNT(*) AS num
FROM
db1.table
WHERE
create_time > DATE_SUB( sysdate(), INTERVAL 20 MINUTE )
GROUP BY
time
ORDER BY
time
) speed;

碰到慢SQL不用多想,第一步先上执行计划:

很明显,这张900多万行的表因为创建了TiFlash副本,在碰到聚合运算的时候优化器选择了走列存查询,最终结果就是在TiFlash完成暴力全表扫描、排序、分组、计算等一系列操作,返回给TiDB Server时基本已经加工完成,总共耗时8.02s。

咋一看好像没啥优化空间,但仔细观察会发现一个不合理的地方。执行计划倒数第二排的Selection算子,也就是SQL里面子查询的where过滤,实际有效数据1855行,却扫描了整个表接近950W行,这是一个典型的适合索引加速的场景。但遗憾的是,在TiFlash里面并没有索引的概念,所以只能默默地走全表扫描。

那么优化的第一步,先看过滤字段是否有索引,通常来说create_time这种十有八九都建过索引,检查后发现确实有。

第二步,尝试让优化器走TiKV查询,这里直接使用hint的方式:

SELECT /*+ READ_FROM_STORAGE(TIKV[db1.table]) */
cast( cast( CAST( SUM( num ) / COUNT( time ) AS CHAR ) AS DECIMAL ( 9, 2 )) AS signed ) speed,
... -- 此处省略n个字段
FROM
(
SELECT
DATE_FORMAT( receive_time, '%Y-%m-%d %H:%i:00' ) AS time,
COUNT(*) AS num
FROM
db1.table
WHERE
create_time > DATE_SUB( sysdate(), INTERVAL 20 MINUTE )
GROUP BY
time
ORDER BY
time
) speed;

再次生成执行计划,发现还是走了TiFlash查询。这里就引申出一个重要知识点,关于hint作用域的问题,也就是说hint只能在指定的查询范围内生效。具体到上面这个例子,虽然指定了db1.table走TiKV查询,但是对于它所在的查询块来说,压根不知道db1.table是谁直接就忽略掉了。所以正确的写法是把hint写到子查询中:

SELECT
cast( cast( CAST( SUM( num ) / COUNT( time ) AS CHAR ) AS DECIMAL ( 9, 2 )) AS signed ) speed,
... -- 此处省略n个字段
FROM
(
SELECT  /*+ READ_FROM_STORAGE(TIKV[db1.table]) */
DATE_FORMAT( receive_time, '%Y-%m-%d %H:%i:00' ) AS time,
COUNT(*) AS num
FROM
db1.table
WHERE
create_time > DATE_SUB( sysdate(), INTERVAL 20 MINUTE )
GROUP BY
time
ORDER BY
time
) speed;

对应的执行计划为:

小提示:

也可以通过set session tidb_isolation_read_engines = 'tidb,tikv';来让优化器走tikv查询。

发现这次虽然走了TiKV查询,但还是用的TableFullScan算子,整体时间不降反升,和我们预期的有差距。

没走索引那肯定是和查询字段有关系,分析上面SQL的逻辑,开发是想查询table表创建时间在最近20分钟的数据,用了一个sysdate()函数获取当前时间,问题就出在这。

获取当前时间常用的函数有now()sysdate(),但这两者是有明显区别的。引用自官网的解释:

  • now()得到的是语句开始执行的时间,是一个固定值
  • sysdate()得到的是该函数实际执行的时间,是一个动态值

听起来比较饶,来个栗子一看便知:

mysql> select now(),sysdate(),sleep(3),now(),sysdate();
+---------------------+---------------------+----------+---------------------+---------------------+
| now()               | sysdate()           | sleep(3) | now()               | sysdate()           |
+---------------------+---------------------+----------+---------------------+---------------------+
| 2023-03-16 15:55:18 | 2023-03-16 15:55:18 |        0 | 2023-03-16 15:55:18 | 2023-03-16 15:55:21 |
+---------------------+---------------------+----------+---------------------+---------------------+
1 row in set (3.06 sec)

这个动态时间就意味着TiDB优化器在估算的时候并不知道它是个什么值,走索引和不走索引哪个成本更高,最终导致索引失效。

从业务上来看,这个SQL用now()sysdate()都可以,那么就尝试改成now()看看效果:

SELECT
cast( cast( CAST( SUM( num ) / COUNT( time ) AS CHAR ) AS DECIMAL ( 9, 2 )) AS signed ) speed,
... -- 此处省略n个字段
FROM
(
SELECT  /*+ READ_FROM_STORAGE(TIKV[db1.table]) */
DATE_FORMAT( receive_time, '%Y-%m-%d %H:%i:00' ) AS time,
COUNT(*) AS num
FROM
db1.table
WHERE
create_time > DATE_SUB( now(), INTERVAL 20 MINUTE )
GROUP BY
time
ORDER BY
time
) speed;

最终结果4.43ms搞定,从8.02s到4.43ms,1800倍的提升。

滥用函数,属于是开发给自己挖的坑了。

解决方案

经过以上分析,优化思路已经很清晰了,甚至都是常规优化不值得专门拿出来讲,但前后效果差异太大,很适合作为一个反面教材来提醒大家认真写SQL。

其实就两点:

  • 让优化器不要走TiFlash查询,改走TiKV,可通过hint或SQL binding解决
  • 非必须不要使用动态时间,避免带来索引失效的问题

深度思考

优化完成之后,我开始思考优化器走错执行计划的原因。

在最开始的执行计划当中,优化器对Selection算子的估算值estRows和实际值actRows相差非常大,再加上本身计算和聚合比较多,这可能是导致误走TiFlash的原因之一。不清楚TiFlash的estRows计算原理是什么,如果在估算准确的情况并且索引正常的情况下会不会走TiKV呢?

另外,我还怀疑过动态时间导致优化器判断失误(认为索引失效才选择走TiFlash),但是在尝试只修改sysdate()now()的情况下,发现依然走了TiFlash,说明这个可能性不大。

在索引字段没问题的时候,按正常逻辑来说,我觉得一个成熟的优化器应该要能够判断出这种场景走TiKV更好。

总结

TiFlash虽然是个好东西,但是优化器还在进化当中,难免有判断失误的时候,那么会导致适得其反的效果,我们要及时通过人工手段介入。再给TiDB优化器一些时间。

良好的SQL习惯至关重要,这也是老生常谈的问题了,再好的数据库也扛不住乱造的SQL。

作者介绍:hey-hoho,来自神州数码钛合金战队,是一支致力于为企业提供分布式数据库TiDB整体解决方案的专业技术团队。团队成员拥有丰富的数据库从业背景,全部拥有TiDB高级资格证书,并活跃于TiDB开源社区,是官方认证合作伙伴。目前已为10+客户提供了专业的TiDB交付服务,涵盖金融、证券、物流、电力、政府、零售等重点行业。

TiDB SQL调优案例之避免TiFlash帮倒忙的更多相关文章

  1. ORACLE SQL调优案例一则

    收到监控告警日志文件(Alert)的作业发出的告警邮件,表空间TEMPSCM2不能扩展临时段,说明临时表空间已经被用完了,TEMPSCM2表空间不够用了 Dear All:   The Instanc ...

  2. 《高性能SQL调优精要与案例解析》——10.4_SQL语句改写部分文档

    应各位读者要求,现将<高性能SQL调优精要与案例解析>中<10.4 SQL语句改写>部分整理成电子文档,上传至群共享文件(群号:298176197): 或者通过如下链接下载: ...

  3. 《高性能SQL调优精要与案例解析》一书谈主流关系库SQL调优(SQL TUNING或SQL优化)核心机制之——索引(index)

    继<高性能SQL调优精要与案例解析>一书谈SQL调优(SQL TUNING或SQL优化),我们今天就谈谈各主流关系库中,占据SQL调优技术和工作半壁江山的.最重要的核心机制之一——索引(i ...

  4. 《高性能SQL调优精要与案例解析》一书谈SQL调优(SQL TUNING或SQL优化)学习

    <高性能SQL调优精要与案例解析>一书上市发售以来,很多热心读者就该书内容及一些具体问题提出了疑问,因读者众多外加本人日常工作的繁忙 ,在这里就SQL调优学习进行讨论并对热点问题统一作答. ...

  5. 必读,sql加索引调优案例和explain extended说明

    做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 昨天分享了Mysql中的 explain 命令,使用 explain 来分析 select 语句的运行效果,如 :expl ...

  6. SQL调优

    # 问题的提出 在应用系统开发初期,由于开发数据库数据比较少,对于查询SQL语句,复杂视图的的编写等体会不出SQL语句各种写法的性能优劣,但是如果将应用 系统提交实际应用后,随着数据库中数据的增加,系 ...

  7. SQL调优日志--内存问题

    SQL调优日志--内存问题排查入门篇   概述 很多系统的性能问题,是由内存导致的.内存不够会导致页面频繁换入换出,IO队列高,进而影响数据库整体性能. 排查 内存对数据库性能非常重要.那么我当出现问 ...

  8. 梁敬彬老师的《收获,不止SQL优化》,关于如何缩短SQL调优时间,给出了三个步骤,

    梁敬彬老师的<收获,不止SQL优化>,关于如何缩短SQL调优时间,给出了三个步骤, 1. 先获取有助调优的数据库整体信息 2. 快速获取SQL运行台前信息 3. 快速获取SQL关联幕后信息 ...

  9. SQL调优常用方法

    在使用DBMS时经常对系统的性能有非常高的要求:不能占用过多的系统内存和 CPU资源.要尽可能快的完成的数据库操作.要有尽可能高的系统吞吐量.如果系统开发出来不能满足要求的所有性能指标,则必须对系统进 ...

  10. 读《程序员的SQL金典》[4]--SQL调优

    一.SQL注入 如果程序中采用sql拼接的方式书写代码,那么很可能存在SQL注入漏洞.避免的方式有两种: 1. 对于用户输入过滤敏感字母: 2. 参数化SQL(推荐). 二.索引 ①索引分类 聚簇索引 ...

随机推荐

  1. DER编码

    1. Attribute Type编码 ​ 对于标识串,采用低标识编码方式,只需1个字节.OBJECT IDENTIFIER的 tag为Ox06;class选择universal,则位8和位7为0,O ...

  2. linux 基线检查

    1 检查用户缺省UMASK #cat /etc/profile|sed '/^#/d'|sed '/^$/d'|grep -i "umask" 修改umask vi /etc/pr ...

  3. 微服务注册到Nacos上的Ip错误,是内网ip不是公网ip

    spring.cloud.nacos.discovery.ip = 本机公网IP spring.cloud.nacos.discovery.port = 服务端口

  4. pads:数据格式不正确,网络必须包含一个以上管脚

    1,如果已经有pcb封装,则在pads logic软件里面-元件编辑器-重新做封装,在--编辑电参数--里面匹配对应pcb封装, 2,点击-工具--,--从库中更新--,更新一下,之后导入pcb la ...

  5. 04 Hadoop思想与原理

    Hadoop最早起源于Nutch.Nutch的设计目标是构建一个大型的全网搜索引擎,包括网页抓取.索引.查询等功能,但随着抓取网页数量的增加,遇到了严重的可扩展性问题--如何解决数十亿网页的存储和索引 ...

  6. toString能转换number类型吗

    let num = 60console.log(toString(num)) // [object Undefined] console.log(String(num)) // 60

  7. Matlab字体设置中找不到字体的解决方法(转载)

    Matlab字体设置中找不到字体 Matlab默认的字体实在不好看,一般都需要重新设置字体. 在其字体设置中有些字体不能同时支持中文和英文,我在之前的博客中说过,如何为Matlab设置一款好看的同时兼 ...

  8. 查看app包名

    操作步骤: 1.cmd中输入命令:adb shell am monitor 2.启动需要获取包名的应用

  9. 微信小程序 图片预览 wx.previewImage

    官网地址: go官网 效果展示: list: [ 'http://img5.imgtn.bdimg.com/it/u=3300305952,1328708913&fm=26&gp=0. ...

  10. .net core 版本避坑 无法打开项目文件。 无法找到 .NET SDK。请检查确保已安装此项且 global.json 中指定的版本(如有)与所安装的版本相匹配。

    打开项目根目录,输入 dotnet --version  ,查看本机安装的最高版本的sdk: 如果源文件中存在global.json文件,直接打开修改即可: 不存在则接着在1步骤后输入: dotnet ...