18 | 为什么这些SQL语句逻辑相同,性能却差异巨大?

Page content

18 | 为什么这些SQL语句逻辑相同,性能却差异巨大?

Mysql 规定,如果对字段做了函数计算,无法使用索引

建表语句

CREATE TABLE `tradelog` (
  `id` int(11) NOT NULL,
  `tradeid` varchar(32) DEFAULT NULL,
  `operator` int(11) DEFAULT NULL,
  `t_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `tradeid` (`tradeid`),
  KEY `t_modified` (`t_modified`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

查询语句

select count(*) from tradelog where month(t_modified)=7;

隐式类型转换相当于调用了 CAST 函数,无法使用索引

查询语句

select * from tradelog where tradeid=110717;

相当于调用

select * from tradelog where  CAST(tradid AS signed int) = 110717;

隐式字符编码转换相当于调用了 CONVERT 函数,无法使用索引

  • 建表语句

    CREATE TABLE `trade_detail` (
    `id` int(11) NOT NULL,
    `tradeid` varchar(32) DEFAULT NULL,
    `trade_step` int(11) DEFAULT NULL, /* 操作步骤 */
    `step_info` varchar(32) DEFAULT NULL, /* 步骤信息 */
    PRIMARY KEY (`id`),
    KEY `tradeid` (`tradeid`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
  • 查询语句

    select d.* from tradelog l, trade_detail d where d.tradeid=l.tradeid and l.id=2;
    
  • explain 结果

    第一行显示优化器会先在交易记录表 tradelog 上查到 id=2 的行,这个步骤用上了主键索引,rows=1 表示只扫描一行;

    第二行 key=NULL,表示没有用上交易详情表 trade_detail 上的 tradeid 索引,进行了全表扫描。

  • 大致执行过程 先去 tradelog 中取出 tradeid 字段,再去 trade_detail 里查询匹配字段;tradelog 称为驱动表,trade_detail 称为被驱动表

  • 查询语句相当于执行

    select * 
    from trade_detail 
    where tradeid=$L2.tradeid.value;
    

    其中,$L2.tradeid.value 的字符集是 utf8mb4

  • 两者字符集不匹配,需要将 utf8 转成 utf8mb4相当于需要执行

    select * 
    from trade_detail  
    where CONVERT(traideid USING utf8mb4)=$L2.tradeid.value; 
    

如何解决因为函数计算导致的无法使用索引问题?

尽量避免函数计算

设法将函数计算转移到输入参数上

比如将查询更改为:

select l.operator 
from tradelog l , trade_detail d 
where d.tradeid=l.tradeid and d.id=4;

相当于执行

select operator 
from tradelog  
where traideid =$R4.tradeid.value;

因为 $R4.tradeid.value 的字符集是 utf8, 按照字符集转换规则,要转成 utf8mb4

问题讨论

你遇到过别的、类似今天我们提到的性能问题吗?你认为原因是什么,又是怎么解决的呢?

建表语句

CREATE TABLE `table_a` (
  `id` int(11) NOT NULL,
  `b` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `b` (`b`)
) ENGINE=InnoDB;

假设现在表里面,有 100 万行数据,其中有 10 万行数据的 b 的值是’1234567890’, 假设现在执行语句是这么写的: select * from table_a where b='1234567890abcd';

执行流程如下:

  • 在传给引擎执行的时候,做了字符截断。因为引擎里面这个行只定义了长度是 10,所以只截了前 10 个字节,就是’1234567890’进去做匹配;
  • 这样满足条件的数据有 10 万行;
  • 因为是 select *, 所以要做 10 万次回表;
  • 但是每次回表以后查出整行,到 server 层一判断,b 的值都不是’1234567890abcd’;
  • 返回结果是空。

虽然执行过程中可能经过函数操作,但是最终在拿到结果后,server 层还是要做一轮判断的