为什么Mysql explain extended中的filtered列值总是100%

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 1. 问题 执行Mysql的explain extended的输出会比单纯的explain多一列filtered(MySQL 5.7缺省就会输出filtered),它指返回结果的行占需要读到的行(rows列的值)的百分比。

1. 问题

执行Mysql的explain extended的输出会比单纯的 explain多一列filtered(MySQL 5.7缺省就会输出 filtered),它指返回结果的行占需要读到的行(rows列的值)的百分比。按说 filtered是个非常有用的值,因为对于join操作,前一个表的结果集大小直接影响了循环的次数。但是我的环境下测试的结果却是, filtered的值一直是100%,也就是说失去了意义

参考下面mysql 5.6的代码, filtered值只对index和all的扫描有效(这可以理解,其它场合,通常rows值就等于估算的结果集大小。)。
sql/opt_explain.cc

点击(此处)折叠或打开

  1. bool Explain_join::explain_rows_and_filtered()
  2. {
  3.   if (table->pos_in_table_list->schema_table)
  4.     return false;

  5.   double examined_rows;
  6.   if (select && select->quick)
  7.     examined_rows= rows2double(select->quick->records);
  8.   else if (tab->type == JT_INDEX_SCAN || tab->type == JT_ALL)
  9.   {
  10.     if (tab->limit)
  11.       examined_rows= rows2double(tab->limit);
  12.     else
  13.     {
  14.       table->pos_in_table_list->fetch_number_of_rows();
  15.       examined_rows= rows2double(table->file->stats.records);
  16.     }
  17.   }
  18.   else
  19.     examined_rows= tab->position->records_read;

  20.   fmt->entry()->col_rows.set(static_castlonglong>(examined_rows));

  21.   /* Add "filtered" field */
  22.   if (describe(DESCRIBE_EXTENDED))
  23.   {
  24.     float f= 0.0;
  25.     if (examined_rows)
  26.       f= 100.0 * tab->position->records_read / examined_rows;
  27.     fmt->entry()->col_filtered.set(f);
  28.   }
  29.   return false;
  30. }

但是,我构造了一个全表扫描后, filtered的结果却不对,仍然是100%,而我期待的是0.1%。

点击(此处)折叠或打开

  1. mysql> desc tb2;
    +-------+--------------+------+-----+---------+-------+
    | Field | Type         | Null | Key | Default | Extra |
    +-------+--------------+------+-----+---------+-------+
    | id    | int(11)      | NO   | PRI | 0       |       |
    | c1    | int(11)      | YES  |     | NULL    |       |
    | c2    | varchar(100) | YES  |     | NULL    |       |
    +-------+--------------+------+-----+---------+-------+
    3 rows in set (0.00 sec)

    mysql> explain extended select * from tb2 where c1 +----+-------------+-------+------+---------------+------+---------+------+--------+----------+-------------+
    | id | select_type | table | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
    +----+-------------+-------+------+---------------+------+---------+------+--------+----------+-------------+
    |  1 | SIMPLE      | tb2   | ALL  | NULL          | NULL | NULL    | NULL | 996355 |   100.00 | Using where |
    +----+-------------+-------+------+---------------+------+---------+------+--------+----------+-------------+
    1 row in set, 1 warning (10 min 29.96 sec)

    mysql> select count(*) from tb2 where c1 +----------+
    | count(*) |
    +----------+
    |     1001 |
    +----------+
    1 row in set (1.99 sec)

通过gdb跟踪,发现代码走的分支是对的,但下面的值有问题。

点击(此处)折叠或打开

  1. (gdb) p table->file->stats.records
  2. $18 = 996355
  3. (gdb) p tab->position->records_read
  4. $19 = 996355
上面的tab->position->records_read应该是估算出的返回行数,正确的值应该是1001左右,而不是全表大小 996355

2.原因

为什么会出现上面的情况呢?后来我查看了下MySQL收集的统计信息就明白了。
MySQL和其它主流数据库一样会自动需要收集统计信息以便生成更好的执行计划,也可以用analyze table手动收集, 收集的统计信息存储在mysql.innodb_table_stats和mysql.innodb_index_stats里
参考:http://dev.mysql.com/doc/refman/5.6/en/innodb-persistent-stats.html#innodb-persistent-stats-tables

但这不是重点,
重点是,查看这两个表就会发现MySQL收集的统计信息非常少

点击(此处)折叠或打开

  1. mysql> select * from mysql.innodb_table_stats where table_name='tb2';
    +---------------+------------+---------------------+--------+----------------------+--------------------------+
    | database_name | table_name | last_update         | n_rows | clustered_index_size | sum_of_other_index_sizes |
    +---------------+------------+---------------------+--------+----------------------+--------------------------+
    | test          | tb2        | 2015-12-02 06:26:54 | 996355 |                 3877 |                        0 |
    +---------------+------------+---------------------+--------+----------------------+--------------------------+
    1 row in set (0.00 sec)

    mysql> select * from mysql.innodb_index_stats where table_name='tb2';
    +---------------+------------+------------+---------------------+--------------+------------+-------------+-----------------------------------+
    | database_name | table_name | index_name | last_update         | stat_name    | stat_value | sample_size | stat_description                  |
    +---------------+------------+------------+---------------------+--------------+------------+-------------+-----------------------------------+
    | test          | tb2        | PRIMARY    | 2015-12-02 06:26:54 | n_diff_pfx01 |     996355 |          20 | id                                |
    | test          | tb2        | PRIMARY    | 2015-12-02 06:26:54 | n_leaf_pages |       3841 |        NULL | Number of leaf pages in the index |
    | test          | tb2        | PRIMARY    | 2015-12-02 06:26:54 | size         |       3877 |        NULL | Number of pages in the index      |
    +---------------+------------+------------+---------------------+--------------+------------+-------------+-----------------------------------+
    3 rows in set (0.00 sec)
重要的信息也就2个,一是表的总记录数( n_rows),二是索引中的列的唯一值数( n_diff_pfx01)。也就是说MySQL不会统计非索引列的值分布信息,在前面的查询的例子中,由于c1没有被索引,所以MySQL无法估算出" c1"会最终筛选出多少记录。这样一来,filtered的值真正有效的场合就非常少了,要where条件中出现的列上建有索引,又要执行计划不走索引的range或ref扫描,而走全表扫描或覆盖索引扫描,所以可以说这个filtered列几乎无用。

3. 引申

后面我联系到MySQL匮乏的统计信息会带来什么后果?
不难想象,如果缺少索引,MySQL很可能会生成性能糟糕的执行计划,比如搞错大表和小表的join顺序,就像下面这样。
  1. mysql> explain extended select count(*) from tb1,tb2 where tb1.c1=tb2.c1 and tb2.c2='xx';
    +----+-------------+-------+------+---------------+------+---------+------+--------+----------+----------------------------------------------------+
    | id | select_type | table | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra                                              |
    +----+-------------+-------+------+---------------+------+---------+------+--------+----------+----------------------------------------------------+
    |  1 | SIMPLE      | tb1   | ALL  | NULL          | NULL | NULL    | NULL |   1000 |   100.00 | NULL                                               |
    |  1 | SIMPLE      | tb2   | ALL  | NULL          | NULL | NULL    | NULL | 996355 |   100.00 | Using where; Using join buffer (Block Nested Loop) |
    +----+-------------+-------+------+---------------+------+---------+------+--------+----------+----------------------------------------------------+
    2 rows in set, 1 warning (0.00 sec)
虽然t1表时小表,tb2表是大表,但是tb2上加上 tb2.c2='xx' 的条件限制后结果集就变成0了,因此先扫描tb2表才是性能更好的选择
相同的查询,PostgreSQL给出的执行计划是更好的,先扫描t2表再循环扫描t1表

  1. postgres=# explain select count(*) from tb1,tb2 where tb1.c1=tb2.c1 and tb2.c2='xx';
  2.                             QUERY PLAN
  3. -------------------------------------------------------------------
  4.  Aggregate (cost=20865.50..20865.51 rows=1 width=0)
  5.    -> Nested Loop (cost=0.00..20865.50 rows=1 width=0)
  6.          Join Filter: (tb1.c1 = tb2.c1)
  7.          -> Seq Scan on tb2 (cost=0.00..20834.00 rows=1 width=4)
  8.                Filter: ((c2)::text = 'xx'::text)
  9.          -> Seq Scan on tb1 (cost=0.00..19.00 rows=1000 width=4)
  10. (6 rows)
下面实际对比一下执行时间看看。

MySQL花了0.34s

  1. mysql> select count(*) from tb1,tb2 where tb1.c1=tb2.c1 and tb2.c2='xx';
  2. +----------+
  3. | count(*) |
  4. +----------+
  5. | 0        |
  6. +----------+
  7. 1 row in set (0.34 sec)

PostgreSQL花了0.139s
  1. postgres=# select count(*) from tb1,tb2 where tb1.c1=tb2.c1 and tb2.c2='xx';
  2.  count
  3. -------
  4.      0
  5. (1 row)

  6. Time: 139.600 ms

上面这个例子的性能 差别其实不是很大,如果去掉 tb2 . c2 = 'xx' 的条件,差别就非常大了。
Mysql花了1分08秒

点击(此处)折叠或打开

  1. mysql> explain select count(*) from tb1,tb2 where tb1.c1=tb2.c1;
    +----+-------------+-------+------+---------------+------+---------+------+--------+----------------------------------------------------+
    | id | select_type | table | type | possible_keys | key  | key_len | ref  | rows   | Extra                                              |
    +----+-------------+-------+------+---------------+------+---------+------+--------+----------------------------------------------------+
    |  1 | SIMPLE      | tb1   | ALL  | NULL          | NULL | NULL    | NULL |   1000 | NULL                                               |
    |  1 | SIMPLE      | tb2   | ALL  | NULL          | NULL | NULL    | NULL | 996355 | Using where; Using join buffer (Block Nested Loop) |
    +----+-------------+-------+------+---------------+------+---------+------+--------+----------------------------------------------------+
    2 rows in set (0.00 sec)

    mysql> select count(*) from tb1,tb2 where tb1.c1=tb2.c1;
    +----------+
    | count(*) |
    +----------+
    |     9949 |
    +----------+
    1 row in set (1 min 8.26 sec)

PostgreSQL只用了0.163秒
  1. postgres=# explain select count(*) from tb1,tb2 where tb1.c1=tb2.c1;
  2.                                QUERY PLAN
  3. -------------------------------------------------------------------------
  4.  Aggregate (cost=23502.34..23502.35 rows=1 width=0)
  5.    -> Hash Join (cost=31.50..23474.97 rows=10947 width=0)
  6.          Hash Cond: (tb2.c1 = tb1.c1)
  7.          -> Seq Scan on tb2 (cost=0.00..18334.00 rows=1000000 width=4)
  8.          -> Hash (cost=19.00..19.00 rows=1000 width=4)
  9.                -> Seq Scan on tb1 (cost=0.00..19.00 rows=1000 width=4)
  10. (6 rows)

  11. Time: 0.690 ms
  12. postgres=# select count(*) from tb1,tb2 where tb1.c1=tb2.c1;
  13.  count
  14. -------
  15.  10068
  16. (1 row)

  17. Time: 163.868 ms

不过这个性能差别和统计信息无关,原因在于PG支持
Nest Loop Join, Merge Join和Hash Join,而MySQL只支持Nest Loop Join,缺了索引Nest Loop Join会慢得跟龟似的

4. 总结

1. MySQL的统计信息非常少,只有表行数和索引列的唯一值数目,这使得MySQL的优化器经常不能对数据规模有一个正确的认识而给出性能不佳的执行计划。
2.  MySQL的join操作的效率 非常依赖于索引( 我之前两次帮人调优MySQL的SQL语句都是在加索引)。并不是说 PG的join不需要索引,只是不像MySQL缺了索引的反应那么大。上面那个MySQL执行了1分多钟的例子,加上索引后,不管是MySQL还是PG的执行时间都立刻降到10毫秒以内。所以, 开发人员在设计表的时候应该对可能的查询方式做个评估,把该建的索引都建上(不能少建也不宜多建)。
3.  相比之下,PG不仅统计所有列的值分布,而且除了唯一值还有直方图,频繁值等等信息,支撑 了PG的优化器做出正确的决策 猜测也是由于这个原因,PG社区认为 PG的优化器已经足够智能,不需要把和Oracle类似的 hint功能加到PG的内核里(因为 hint可能会被人滥用,导致系统很难维护; 不过,实在想用的话可以自己装pg_hint_plan插件)。
相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
1月前
|
SQL 关系型数据库 MySQL
MySQL EXPLAIN该如何分析?
本文将详细介绍MySQL中`EXPLAIN`关键字的工作原理及结果字段解析,帮助优化查询性能。`EXPLAIN`可显示查询SQL的执行计划,其结果包括`id`、`select_type`、`table`等字段。通过具体示例和优化建议,帮助你理解和应用`EXPLAIN`,提升数据库查询效率。
61 0
|
4月前
|
SQL 关系型数据库 MySQL
mysql性能调优:EXPLAIN命令21
【7月更文挑战第21天】掌握SQL性能调优:深入解析EXPLAIN命令的神奇用法!
58 1
|
4月前
|
SQL 缓存 关系型数据库
MySQL|浅谈explain的使用
【7月更文挑战第11天】
|
4月前
|
SQL 存储 数据库
MySQL设计规约问题之性能分析工具如Sql explain、show profile和mysqlsla在数据库性能优化中有什么作用
MySQL设计规约问题之性能分析工具如Sql explain、show profile和mysqlsla在数据库性能优化中有什么作用
|
4月前
|
SQL 索引 关系型数据库
MySQL设计规约问题之为什么推荐使用EXPLAIN来检查SQL查询
MySQL设计规约问题之为什么推荐使用EXPLAIN来检查SQL查询
|
5月前
|
SQL 关系型数据库 MySQL
MySQL数据库——索引(4)-SQL性能分析-profile详情、explain(profile查看指令,explain执行计划中各个字段的含义)
MySQL数据库——索引(4)-SQL性能分析-profile详情、explain(profile查看指令,explain执行计划中各个字段的含义)
62 2
|
23天前
|
存储 关系型数据库 MySQL
Mysql(4)—数据库索引
数据库索引是用于提高数据检索效率的数据结构,类似于书籍中的索引。它允许用户快速找到数据,而无需扫描整个表。MySQL中的索引可以显著提升查询速度,使数据库操作更加高效。索引的发展经历了从无索引、简单索引到B-树、哈希索引、位图索引、全文索引等多个阶段。
56 3
Mysql(4)—数据库索引
|
9天前
|
关系型数据库 MySQL Linux
在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。
本文介绍了在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。同时,文章还对比了编译源码安装与使用 RPM 包安装的优缺点,帮助读者根据需求选择最合适的方法。通过具体案例,展示了编译源码安装的灵活性和定制性。
45 2
|
12天前
|
存储 关系型数据库 MySQL
MySQL vs. PostgreSQL:选择适合你的开源数据库
在众多开源数据库中,MySQL和PostgreSQL无疑是最受欢迎的两个。它们都有着强大的功能、广泛的社区支持和丰富的生态系统。然而,它们在设计理念、性能特点、功能特性等方面存在着显著的差异。本文将从这三个方面对MySQL和PostgreSQL进行比较,以帮助您选择更适合您需求的开源数据库。
52 4