了解 rows_examined_per_scan 和 rows_produced_per_join
Understanding rows_examined_per_scan and rows_produced_per_join
我不是 mysql 人,但我最近有机会使用它 - 我必须优化 Mysql5.7 上的一些查询(我必须介绍一下同样在 5.6 上,但从 5.7 开始,因为它在解释中显然有更多信息) 运行 on AWS Aurora。这需要很多时间,涉及一些连接等。我从切割分支开始,选择仅用两个 table 开始 'debugging'。这些不是太大(~2M 和~1.5M 行),但是,一般来说我认为它们设计得不太大(varchar(255) 列上的主键等)。
问题是我想看一看所以我使用了 explain format=json
并且我试图从中获得任何见解。
所以,假设
select cc.id, cc.col1, cc.col2, ct.col1
from my_table cc
inner join my_table ct on ct.cc_id = cc.id
我得到了类似
的东西
{
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "5630369.56"
},
"nested_loop": [
{
"table": {
"table_name": "cc",
"access_type": "index",
"possible_keys": [
"PRIMARY"
],
"key_length": "258",
"rows_examined_per_scan": 1248725,
"rows_produced_per_join": 1248725,
"filtered": "100.00",
"using_index": true,
"cost_info": {
"read_cost": "3732979.00",
"eval_cost": "249745.00",
"prefix_cost": "3982724.00",
"data_read_per_join": "1G"
}
}
},
{
"table": {
"table_name": "ct",
"access_type": "ref",
"possible_keys": [
"cc_id_idx"
],
"key": "cc_id_idx",
"key_length": "257",
"ref": [
"cc.id"
],
"rows_examined_per_scan": 1,
"rows_produced_per_join": 1373037,
"filtered": "100.00",
"cost_info": {
"read_cost": "1373037.97",
"eval_cost": "274607.59",
"prefix_cost": "5630369.56",
"data_read_per_join": "3G"
},
"used_columns": [
"id",
"col_1",
"col_2",
...
]
}
}
]
}
}
我在理解这里到底发生了什么时遇到了问题。我认为 mysql 在以 table cc
开始的地方进行嵌套连接,然后对于它命中的每一行 ct
(access_type
是 ref
和 "rows_examined_per_scan": 1
)。它使它> 1M次("rows_produced_per_join": 1373037
)。那是对的吗?我正在为此寻找任何文档,但我没有找到任何关于如何在加入上下文中读取这些值的具体信息 - 也许只是我的 google-fu 不够强大。有没有人能给我一些线索?
(因为我正在寻找加速它的任何选项,所以我想强制 mysql 进行散列连接,这在 Aurora 上可用(我在 Aurora 2.09 上尝试过)形式为/*+ HASH_JOIN(cc) */
,但它在任何情况下都不会影响查询计划 - 但它是另一个问题的问题。)
select cc.id, cc.col1, cc.col2, ct.col1
from my_table cc
inner join my_table ct on ct.cc_id = cc.id
如果有 WHERE
子句,那 可能 会导致优化器选择 WHERE
中提到的 table 作为“第一个" table 在 JOIN
.
没有 WHERE
,优化器 通常 会选择较小的 table。作为“第一”。
然后它通常 执行 NLJ(嵌套循环连接):
- 读取“第一个”table 的所有行(可能被
WHERE
过滤)。
- 对于这些行中的每一行,它都会到达另一行 table (NLJ)。
第 2 步 有时 通过将整个“第二个”table 读入内存并构建哈希来完成。但这只适用于“小”秒 table。您的 table 似乎太大了。
我提出所有这些是因为您提出的问题得到了简化; 'real' 查询可能不会像 EXPLAIN
.
那样执行
一些观察:
- "key_length" 是“大”且不一致的 -- 检查声明,包括排序规则。当一些合理的长度可行时,不要盲目地使用
VARCHAR(255)
。在 JOIN
中混合排序规则对性能来说是致命的。 (这并不能真正证明查询有这个问题。)
- 虽然我只是批评
VARCHAR(255)
的 PRIMARY KEY
,但我不同意那些惊呼它很糟糕并且你“必须”切换到 INT
的人。你能告诉我们那里有什么类型的数据吗? UUID vs 短字符串 vs URL vs ...——不同的优化技术可能适用,具体取决于数据。
- "using_index": true -- 这意味着 all 查询中 table 所需的列在 [=22] =] 正在使用。这对性能有好处。但是,如果您在
SELECT
子句中添加另一列,则可以取消它。
- "used_columns" -- 列表似乎比查询需要的长?索引中有很多列吗?
EXPLAIN
是否与查询不匹配?
- “散列”很少比“BTree”好。 (我不知道Aurora的具体情况,所以对于你试过的index hint我也不好说什么。)
所写的简单查询将花费很长时间 -- 扫描一个 table(或索引),再加上对另一个 table.[=28 的大量 BTree 查找=]
回到你原来的问题。这是我在解释中看到的内容:
- “嵌套循环”== NLJ,正如预期和上面所讨论的那样。
- 大笔费用 -- 加入大 tables
时可以预料
- “使用索引”== 很好,但唯一“可能的键”是“PRIMARY”。所以,它实际上是一个 table 扫描,而不是真正的“索引扫描”。
- "Key_length" -- 见上文
- "Used_columns" -- 见上文
- access_type 是 ref 和 "rows_examined_per_scan": 1 -- 查找(见上文)。但请注意,“rows_scanned”是一个估计值。实际值取决于
cc_id
是否为 UNIQUE
.
- "rows_produced_per_join" -- 没那么重要
如有此类问题,请提供SHOW CREATE TABLE
。
我不是 mysql 人,但我最近有机会使用它 - 我必须优化 Mysql5.7 上的一些查询(我必须介绍一下同样在 5.6 上,但从 5.7 开始,因为它在解释中显然有更多信息) 运行 on AWS Aurora。这需要很多时间,涉及一些连接等。我从切割分支开始,选择仅用两个 table 开始 'debugging'。这些不是太大(~2M 和~1.5M 行),但是,一般来说我认为它们设计得不太大(varchar(255) 列上的主键等)。
问题是我想看一看所以我使用了 explain format=json
并且我试图从中获得任何见解。
所以,假设
select cc.id, cc.col1, cc.col2, ct.col1
from my_table cc
inner join my_table ct on ct.cc_id = cc.id
我得到了类似
的东西{
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "5630369.56"
},
"nested_loop": [
{
"table": {
"table_name": "cc",
"access_type": "index",
"possible_keys": [
"PRIMARY"
],
"key_length": "258",
"rows_examined_per_scan": 1248725,
"rows_produced_per_join": 1248725,
"filtered": "100.00",
"using_index": true,
"cost_info": {
"read_cost": "3732979.00",
"eval_cost": "249745.00",
"prefix_cost": "3982724.00",
"data_read_per_join": "1G"
}
}
},
{
"table": {
"table_name": "ct",
"access_type": "ref",
"possible_keys": [
"cc_id_idx"
],
"key": "cc_id_idx",
"key_length": "257",
"ref": [
"cc.id"
],
"rows_examined_per_scan": 1,
"rows_produced_per_join": 1373037,
"filtered": "100.00",
"cost_info": {
"read_cost": "1373037.97",
"eval_cost": "274607.59",
"prefix_cost": "5630369.56",
"data_read_per_join": "3G"
},
"used_columns": [
"id",
"col_1",
"col_2",
...
]
}
}
]
}
}
我在理解这里到底发生了什么时遇到了问题。我认为 mysql 在以 table cc
开始的地方进行嵌套连接,然后对于它命中的每一行 ct
(access_type
是 ref
和 "rows_examined_per_scan": 1
)。它使它> 1M次("rows_produced_per_join": 1373037
)。那是对的吗?我正在为此寻找任何文档,但我没有找到任何关于如何在加入上下文中读取这些值的具体信息 - 也许只是我的 google-fu 不够强大。有没有人能给我一些线索?
(因为我正在寻找加速它的任何选项,所以我想强制 mysql 进行散列连接,这在 Aurora 上可用(我在 Aurora 2.09 上尝试过)形式为/*+ HASH_JOIN(cc) */
,但它在任何情况下都不会影响查询计划 - 但它是另一个问题的问题。)
select cc.id, cc.col1, cc.col2, ct.col1
from my_table cc
inner join my_table ct on ct.cc_id = cc.id
如果有 WHERE
子句,那 可能 会导致优化器选择 WHERE
中提到的 table 作为“第一个" table 在 JOIN
.
没有 WHERE
,优化器 通常 会选择较小的 table。作为“第一”。
然后它通常 执行 NLJ(嵌套循环连接):
- 读取“第一个”table 的所有行(可能被
WHERE
过滤)。 - 对于这些行中的每一行,它都会到达另一行 table (NLJ)。
第 2 步 有时 通过将整个“第二个”table 读入内存并构建哈希来完成。但这只适用于“小”秒 table。您的 table 似乎太大了。
我提出所有这些是因为您提出的问题得到了简化; 'real' 查询可能不会像 EXPLAIN
.
一些观察:
- "key_length" 是“大”且不一致的 -- 检查声明,包括排序规则。当一些合理的长度可行时,不要盲目地使用
VARCHAR(255)
。在JOIN
中混合排序规则对性能来说是致命的。 (这并不能真正证明查询有这个问题。) - 虽然我只是批评
VARCHAR(255)
的PRIMARY KEY
,但我不同意那些惊呼它很糟糕并且你“必须”切换到INT
的人。你能告诉我们那里有什么类型的数据吗? UUID vs 短字符串 vs URL vs ...——不同的优化技术可能适用,具体取决于数据。 - "using_index": true -- 这意味着 all 查询中 table 所需的列在 [=22] =] 正在使用。这对性能有好处。但是,如果您在
SELECT
子句中添加另一列,则可以取消它。 - "used_columns" -- 列表似乎比查询需要的长?索引中有很多列吗?
EXPLAIN
是否与查询不匹配? - “散列”很少比“BTree”好。 (我不知道Aurora的具体情况,所以对于你试过的index hint我也不好说什么。)
所写的简单查询将花费很长时间 -- 扫描一个 table(或索引),再加上对另一个 table.[=28 的大量 BTree 查找=]
回到你原来的问题。这是我在解释中看到的内容:
- “嵌套循环”== NLJ,正如预期和上面所讨论的那样。
- 大笔费用 -- 加入大 tables 时可以预料
- “使用索引”== 很好,但唯一“可能的键”是“PRIMARY”。所以,它实际上是一个 table 扫描,而不是真正的“索引扫描”。
- "Key_length" -- 见上文
- "Used_columns" -- 见上文
- access_type 是 ref 和 "rows_examined_per_scan": 1 -- 查找(见上文)。但请注意,“rows_scanned”是一个估计值。实际值取决于
cc_id
是否为UNIQUE
. - "rows_produced_per_join" -- 没那么重要
如有此类问题,请提供SHOW CREATE TABLE
。