一个优化MySQL查询操作的具体案例分析

问题描述

一个用户反映先线一个SQL语句执行时间慢得无法接受。SQL语句看上去很简单(本文描述中修改了表名和字段名):
SELECT count(*) FROM a JOIN b ON a.`S` = b.`S` WHERE a.`L` > '2014-03-30 00:55:00' AND a.`L` < '2014-03-30 01:00:00' ;

且查询需要的字段都建了索引,表结构如下:

CREATE TABLE `a` (
`L` timestamp NOT NULL DEFAULT '2000-01-01 00:00:00',
`I` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
`A` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
`S` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
`F` tinyint(4) DEFAULT NULL,
`V` varchar(256) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT '',
`N` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
KEY `IX_L` (`L`),
KEY `IX_I` (`I`),
KEY `IX_S` (`S`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `b` (
`R` timestamp NOT NULL DEFAULT '2000-01-01 00:00:00',
`V` varchar(32) DEFAULT NULL,
`U` varchar(32) DEFAULT NULL,
`C` varchar(16) DEFAULT NULL,
`S` varchar(64) DEFAULT NULL,
`I` varchar(64) DEFAULT NULL,
`E` bigint(32) DEFAULT NULL,
`ES` varchar(128) DEFAULT NULL,
KEY `IX_R` (`R`),
KEY `IX_C` (`C`),
KEY `IX_S` (`S`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

从语句看,这个查询计划很自然的,就应该是先用a作为驱动表,先后使用 a.L和b.S这两个索引。而实际上explain的结果却是:

+----+-------------+-------+-------+---------------+------+---------+----------+---------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+------+---------+----------+---------+-------------+
| 1 | SIMPLE | b | index | IX_S | IX_S | 195 | NULL | 1038165 | Using index |
| 1 | SIMPLE | a | ref | IX_L,IX_S | IX_S | 195 | test.b.S | 1 | Using where |
+----+-------------+-------+-------+---------------+------+---------+----------+---------+-------------+

分析

从explain的结果看,查询用了b作为驱动表。

上一篇文章我们介绍到,MySQL选择jion顺序是分别分析各种join顺序的代价后,选择最小代价的方法。

这个join只涉及到两个表,自然也与optimizer_search_depth无关。于是我们的问题就是,我们预期的那个join顺序的为什么没有被选中?

MySQL Tips: MySQL提供straight_join语法,强制设定连接顺序。

explain SELECT count(*) FROM a straight_join b ON a.`S` = b.`S` WHERE a.`L` > '2014-03-30 00:55:00' AND a.`L` < '2014-03-30 01:00:00' ;

+----+-------------+-------+-------+---------------+------+---------+------+---------+---------------------------------------------+

| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |

+----+-------------+-------+-------+---------------+------+---------+------+---------+---------------------------------------------+

| 1 | SIMPLE | a | range | IX_L,IX_S | IX_L | 4 | NULL | 63 | Using where |

| 1 | SIMPLE | b | index | IX_S | IX_S | 195 | NULL | 1038165 | Using where; Using index; Using join buffer |

+----+-------------+-------+-------+---------------+------+---------+------+---------+---------------------------------------------+

MySQL Tips: explain结果中,join的查询代价可以用依次连乘rows估算。

?join顺序对了,简单的分析查询代价:普通join是1038165*1, straight_join是 63*1038165. 貌似MySQL没有错。但一定哪里不对!

发现异常

回到我们最初的设想。我们预计表a作为驱动表,是因为认为表b能够用上IX_S索引,而实际上staight_join的时候确实用上了,但这个结果与我们预期的又不同。

我们知道,索引的过滤性是决定了一个索引在查询中是否会被选中的重要因素,那么是不是b.S的过滤性不好呢?

MySQL Tips: show index from tbname返回结果中Cardinality的值可以表明一个索引的过滤性。

show index的结果太多,也可以从information_schema表中取。

mysql> select * from information_schema.STATISTICS where table_name='b' and index_name='IX_S'\G
*************************** 1. row ***************************
TABLE_CATALOG: def
TABLE_SCHEMA: test
TABLE_NAME: b
NON_UNIQUE: 1
INDEX_SCHEMA: test
INDEX_NAME: IX_S
SEQ_IN_INDEX: 1
COLUMN_NAME: S
COLLATION: A
CARDINALITY: 1038165
SUB_PART: NULL
PACKED: NULL
NULLABLE: YES
INDEX_TYPE: BTREE
COMMENT:
INDEX_COMMENT:

可以这个索引的CARDINALITY: 1038165,已经很大了。那这个表的估算行是多少呢。

show table status like 'b'\G
*************************** 1. row ***************************
Name: b
Engine: InnoDB
Version: 10
Row_format: Compact
Rows: 1038165
Avg_row_length: 114
Data_length: 119160832
Max_data_length: 0
Index_length: 109953024
Data_free: 5242880
Auto_increment: NULL
Create_time: 2014-05-23 00:24:25
Update_time: NULL
Check_time: NULL
Collation: utf8_general_ci
Checksum: NULL
Create_options:
Comment:
1 row in set (0.00 sec)

从Rows: 1038165看出,IX_S这个索引的区分度被认为非常好,已经近似于唯一索引。

MySQL Tips: 在show table status结果中看到的Rows用于表示表的当前行数。对于MyISAM表这是一个精确值,但对InnoDB这是个估算值。

虽然是估算值,但优化器是以此为指导的,也就是说,上面的某个explain里面的数据完全不符合期望:staight_join结果中第二行的rows。

阶段结论

我们发现整个错误的逻辑是这样的:以a为驱动表的执行计划,由于索引b.S的rows估计为1038165导致优化器认为代价大于以b为驱动表。而实际上这个索引的区分度为1.(当然对explan结果比较熟悉的同学会发现,第二行的type字段和Extra字段一起诡异了)

也就是说,straight_join得到的每一行去b中查询的时候,都走了全表扫描。在MySQL里面出现这种情况的最常见的是类型转换。比如一个字符串字段,虽然包含的是全数字,但查询的时候传入的不是字符串格式。

在这个case里面,两个都是字符串。因此,就是字符集相关了。

回到两个表结构,发现S字段的声明差别在于 COLLATE utf8_bin -- 这个就是本case的根本原因了:a表得到的S值是utf8_bin,优化器认为类型不同,无法直接用上索引b.IX_S过滤。

至于为什么还会用上索引,这个是因为覆盖索引带来“误解”。

MySQL Tips:若查询的所有结果能够从某个索引完全得到,则会优先用遍历索引替代遍历数据。

作为验证,

mysql> explain SELECT * FROM a straight_JOIN b ON binary a.`S` = b.`S` WHERE a.`L` > '2014-03-30 00:55:00' AND a.`L` < '2014-03-30 01:00:00' ;

+—-+————-+——-+——-+—————+——+———+——+———+————————————————+

| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |

+—-+————-+——-+——-+—————+——+———+——+———+————————————————+

| 1 | SIMPLE | a | range | IX_L | IX_L | 4 | NULL | 63 | Using where |

| 1 | SIMPLE | b | ALL | IX_S | NULL | NULL | NULL | 1038165 | Range checked for each record (index map: 0x4) |

+—-+————-+——-+——-+—————+——+———+——+———+————————————————+

由于结果是select *, 无法使用覆盖索引,因此第二行的key就显示为NULL. (笔者泪:要是早出这个结果查起来可方便多了)

优化

当然最直接的想法就是修改两个表的S字段的定义,改成相同即可。这个方法可以避免修改业务代码,但DDL代价略大。这里提供两种在SQL语句方面的优化。

1、select count(*) from b join (select s from a WHERE a.`L` > '2014-03-30 00:55:00' AND a.`L` < '2014-03-30 01:00:00') ta on b.S=ta.s;

这个写法比较直观,需要注意最后b.S和ta.S的顺序

2、SELECT count(*) FROM a JOIN b ON binary a.`S` = b.`S` WHERE a.`L` > '2014-03-30 00:55:00' AND a.`L` < '2014-03-30 01:00:00' ;

从前面的分析知道是由于b.S定义为utf8_bin.

MySQL Tips: MySQL中字符集命名规则中, XXX_bin与XXX的区别为大小写是否敏感。

这里我们将A.s全部增加binary限定,先转为小写,就是将临时结果集转成utf8_bin,之后使用b.S匹配时就能够直接利用索引。

其实两个改写方法的本质相同,区别是写法1是隐式转换。理论上说写法2速度更快些。

小结

做join的字段尽量设计为类型完全相同。

(0)

相关推荐

  • sql查询出各科成绩最好的学生信息

    1.相关数据表 Score表  [User]表 SQL语句如下: 复制代码 代码如下: --查询出各科成绩最好的学生信息 --自连接 --SELECT TOP 1 * FROM Score B WHERE B.ScoreName = '数学' ORDER BY B.Score DESC SELECT A.ID,U.Name,A.ScoreName,A.Score FROM Score A,[User]U WHERE UID IN (SELECT TOP 1 UID FROM Score B WH

  • MySQL查询倒数第二条记录实现方法

    有时候会用到查询倒数第二条记录 复制代码 代码如下: last=HolderChangeHistory.find_by_sql (["               SELECT * FROM holder_change_histories                   where treasure_id = ?                   order by id desc                     limit   1,1  ",               

  • MySQL查询和修改auto_increment的方法

    本文实例讲述了MySQL查询和修改auto_increment的方法.分享给大家供大家参考.具体如下: 查询表名为tableName的auto_increment值: 复制代码 代码如下: SELECT AUTO_INCREMENT FROM information_schema.tables WHERE table_name="tableName"; 修改表名为tableName的auto_increment值: 复制代码 代码如下: ALTER TABLE tableName au

  • 如何使用MySQL查询某个列中相同值的数量统计

    数据现在是这样的,我想确定出type列中的news和image....甚至以后有其他值,他们分别有多少个. SELECT type, count(1) AS counts FROM material GROUP BY type count(1),代表统计第一列,写上1 比写 *的效率高! 以上所述就是本文的全部内容了,希望大家能够喜欢.

  • MySql查询时间段的方法

    本文实例讲述了MySql查询时间段的方法.分享给大家供大家参考.具体方法如下: MySql查询时间段的方法未必人人都会,下面为您介绍两种MySql查询时间段的方法,供大家参考. MySql的时间字段有date.time.datetime.timestamp等,往往我们在存储数据的时候将整个时间存在一个字段中,采用datetime类型:也可能采用将日期和时间分离,即一个字段存储date,一个字段存储时间time.无论怎么存储,在实际应用中,很可能会出现包含"时间段"类型的查询,比如一个访

  • SQL如何实现MYSQL的递归查询

    众所周知,目前的mysql版本中并不支持直接的递归查询,但是通过递归到迭代转化的思路,还是可以在一句SQL内实现树的递归查询的.这个得益于Mysql允许在SQL语句内使用@变量.以下是示例代码. 创建表格 CREATE TABLE `treenodes` ( `id` int , -- 节点ID `nodename` varchar (60), -- 节点名称 `pid` int -- 节点父ID ); 插入测试数据 INSERT INTO `treenodes` (`id`, `nodenam

  • 单个select语句实现MySQL查询统计次数

    单个select语句实现MySQL查询统计次数 单个select语句实现MySQL查询统计次数的方法用处在哪里呢?用处太多了,比如一个成绩单,你要查询及格得人数与不及格的人数,怎么一次查询出来?MySQL查询统计次数简单的语句肯定是这样了: 复制代码 代码如下: select a.name,count_neg,count_plus from    (select count(id) as count_plus,name from score2 where score >=60 group by

  • mysql查询昨天 一周前 一月前 一年前的数据

    mysql 昨天 一周前 一月前 一年前的数据 这里主要用到了DATE_SUB, 参考如下 复制代码 代码如下: SELECT * FROM yh_contentwhere inputtime>DATE_SUB(CURDATE(), INTERVAL 1 DAY)where inputtime>DATE_SUB(CURDATE(), INTERVAL 1 WEEK)where inputtime>DATE_SUB(CURDATE(), INTERVAL 1 MONTH)where inp

  • 数据库表的查询操作(实验二)

    [实验目的]:了解SQL语言的使用,进一步理解关系运算,巩固数据库的基础知识. [实验要求]:掌握利用Select语句进行各种查询操作:单表查询.多表连接及查询.嵌套查询.集合查询等. [实验内容] 一.单表查询 1.简单查询 打开查询分析器,根建立teacher表,并加入数据.从teacher表中分别检索出教师的所有信息,以及仅查询教工号.姓名和职称.语句如下: select * from teacher select tno, tname from teacher 如要查询时改变列标题的显示

  • 50条SQL查询技巧、查询语句示例

    Student(S#,Sname,Sage,Ssex) 学生表 Course(C#,Cname,T#) 课程表 SC(S#,C#,score) 成绩表 Teacher(T#,Tname) 教师表   问题: 1.查询"001"课程比"002"课程成绩高的所有学生的学号: 复制代码 代码如下: select a.S# from (select s#,score from SC where C#='001') a,(select s#,score from SC whe

  • SQL大量数据查询的优化及非用like不可时的处理方案

    1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引. 2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如: select id from t where num is null 可以在num上设置默认值0,确保表中num列没有null值,然后这样查询: select id from t where num=0 3.应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放

  • 大幅优化MySQL查询性能的奇技淫巧

    回顾 MySQL / InnoDB 的改善历史.你能很容易发现.在MySQL 5.6稳定版本中从来没有在read-only 这么快的提速,它很容易搞懂,以及在read-only(RO)有着良好的扩张性.也很期待它在read+write(RW)上达到一个较高水平.(特别是在读取数据是数据库主要工作的时候) 然而.我们对于RO在 MySQL 5.6的表现也十分的高兴,在5.7这个版本中,主要工作集中在 read+write (RW)上, 因为在大数据的处理上还没能达到我们的期望.但是RW依赖RO下.

  • mysql查询今天、昨天、近7天、近30天、本月、上一月的SQL语句

    mysql查询今天,昨天,近7天,近30天,本月,上一月数据的方法分析总结: 话说有一文章表article,存储文章的添加文章的时间是add_time字段,该字段为int(5)类型的,现需要查询今天添加的文章总数并且按照时间从大到小排序,则查询语句如下: 复制代码 代码如下: select * from `article` where date_format(from_UNIXTIME(`add_time`),'%Y-%m-%d') = date_format(now(),'%Y-%m-%d')

  • SQL查询出表、存储过程、触发器的创建时间和最后修改时间示例

    --查询建立时间 --表 select * from sysobjects where id=object_id(N'表名') and xtype='U' --表的结构 select * from syscolumns where id=object_id(N'表名') --存储过程 select * from sysobjects where id=object_id(N'dqtx') and xtype='P' --查询最后修改时间 --存储过程 select name,modify_dat

  • MySQL学习笔记3:表的基本操作介绍

    要操作表首先需要选定数据库,因为表是存在于数据库内的 选择数据库 mysql> use school; Database changed 选择好数据库之后,我们就可以在此数据库之中创建表了 创建表 mysql> create table student( -> id int, -> name varchar(20), -> sex boolean -> ); Query OK, 0 rows affected (0.11 sec) create table用于创建表,后

  • 数据库表的查询操作实践演练(实验三)

    继前两次的实验,本次实验以熟练掌握利用select语句进行各种查询操作:单表查询.多表连接及查询.嵌套查询.集合查询等,巩固数据库查询操作. 下面就跟着小编一起练习吧! 在实验一创建并插入数据的表(Student, Course,SC,Teacher,TC)的基础上,完成以下操作. (1)将教师'罗莉'的名字改为'罗莉莉'. 复制代码 代码如下: update Teacher set tname='罗莉莉' where tname='罗莉' (2)将两个同学(数据自己临时设置,用后即删除)的两门

随机推荐