MySQL select 查询在 BOTH where 和降序的情况下变得很慢
MySQL select query gets quite slow with BOTH where and descending order
我有这个 select 查询,ItemType 是 varchar 类型,ItemComments 是 int 类型:
select * from ItemInfo where ItemType="item_type" order by ItemComments desc limit 1
可以看到这个查询有3个条件:
- 其中 'ItemType' 等于特定值;
- 按 'ItemComments'
排序
- 按降序排列
有趣的是,当我 select 具有所有三个条件的行时,它变得非常慢。但是如果我放弃这三个中的任何一个(条件 2 除外),查询运行得非常快。参见:
select * from ItemInfo where ItemType="item_type" order by ItemComments desc limit 1;
/* Affected rows: 0 Found rows: 1 Warnings: 0 Duration for 1 query: 16.318 sec. */
select * from ItemInfo where ItemType="item_type" order by ItemComments limit 1;
/* Affected rows: 0 Found rows: 1 Warnings: 0 Duration for 1 query: 0.140 sec. */
select * from ItemInfo order by ItemComments desc limit 1;
/* Affected rows: 0 Found rows: 1 Warnings: 0 Duration for 1 query: 0.015 sec. */
此外,
- 我正在使用 MySQL 5.7 和 InnoDB 引擎。
- 我已经在 ItemType 和 ItemComments 上创建了索引,table ItemInfo 包含 200 万行。
我搜索了很多可能的解释,比如MySQL支持降序索引、复合索引等等。但是这些仍然不能解释为什么查询 #1 运行缓慢而查询 #2 和 #3 运行良好。
如果有人能帮助我,我将不胜感激。
更新:创建table并解释信息
创建代码:
CREATE TABLE `ItemInfo` (
`ItemID` VARCHAR(255) NOT NULL,
`ItemType` VARCHAR(255) NOT NULL,
`ItemPics` VARCHAR(255) NULL DEFAULT '0',
`ItemName` VARCHAR(255) NULL DEFAULT '0',
`ItemComments` INT(50) NULL DEFAULT '0',
`ItemScore` DECIMAL(10,1) NULL DEFAULT '0.0',
`ItemPrice` DECIMAL(20,2) NULL DEFAULT '0.00',
`ItemDate` DATETIME NULL DEFAULT '1971-01-01 00:00:00',
PRIMARY KEY (`ItemID`, `ItemType`),
INDEX `ItemDate` (`ItemDate`),
INDEX `ItemComments` (`ItemComments`),
INDEX `ItemType` (`ItemType`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
解释结果:
mysql> explain select * from ItemInfo where ItemType="item_type" order by ItemComments desc limit 1;
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+
| 1 | SIMPLE | i | NULL | index | ItemType | ItemComments | 5 | NULL | 83 | 1.20 | Using where |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+
mysql> explain select * from ItemInfo where ItemType="item_type" order by ItemComments limit 1;
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+
| 1 | SIMPLE | i | NULL | index | ItemType | ItemComments | 5 | NULL | 83 | 1.20 | Using where |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+
mysql> explain select * from ItemInfo order by ItemComments desc limit 1;
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------+
| 1 | SIMPLE | i | NULL | index | NULL | ItemComments | 5 | NULL | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------+
来自 O. Jones 的查询:
mysql> explain
-> SELECT a.*
-> FROM ItemInfo a
-> JOIN (
-> SELECT MAX(ItemComments) ItemComments, ItemType
-> FROM ItemInfo
-> GROUP BY ItemType
-> ) maxcomm ON a.ItemType = maxcomm.ItemType
-> AND a.ItemComments = maxcomm.ItemComments
-> WHERE a.ItemType = 'item_type';
+----+-------------+------------+------------+-------+----------------------------------------+-------------+---------+---------------------------+---------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+-------+----------------------------------------+-------------+---------+---------------------------+---------+----------+--------------------------+
| 1 | PRIMARY | a | NULL | ref | ItemComments,ItemType | ItemType | 767 | const | 27378 | 100.00 | Using where |
| 1 | PRIMARY | <derived2> | NULL | ref | <auto_key0> | <auto_key0> | 772 | mydb.a.ItemComments,const | 10 | 100.00 | Using where; Using index |
| 2 | DERIVED | ItemInfo | NULL | index | PRIMARY,ItemDate,ItemComments,ItemType | ItemType | 767 | NULL | 2289466 | 100.00 | NULL |
+----+-------------+------------+------------+-------+----------------------------------------+-------------+---------+---------------------------+---------+----------+--------------------------+
我不确定我是否正确执行了这个查询,但是我在很长一段时间内无法获取记录。
来自 Vijay 的查询。但是我添加了 ItemType 连接条件,因为只有 max_comnt return 个来自其他 ItemType 的项目:
SELECT ifo.* FROM ItemInfo ifo
JOIN (SELECT ItemType, MAX(ItemComments) AS max_comnt FROM ItemInfo WHERE ItemType="item_type") inn_ifo
ON ifo.ItemComments = inn_ifo.max_comnt and ifo.ItemType = inn_ifo.ItemType
/* Affected rows: 0 Found rows: 1 Warnings: 0 Duration for 1 query: 7.441 sec. */
explain result:
+----+-------------+------------+------------+-------------+-----------------------+-----------------------+---------+-------+-------+----------+-----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+-------------+-----------------------+-----------------------+---------+-------+-------+----------+-----------------------------------------------------+
| 1 | PRIMARY | <derived2> | NULL | system | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 1 | PRIMARY | ifo | NULL | index_merge | ItemComments,ItemType | ItemComments,ItemType | 5,767 | NULL | 88 | 100.00 | Using intersect(ItemComments,ItemType); Using where |
| 2 | DERIVED | ItemInfo | NULL | ref | ItemType | ItemType | 767 | const | 27378 | 100.00 | NULL |
+----+-------------+------------+------------+-------------+-----------------------+-----------------------+---------+-------+-------+----------+-----------------------------------------------------+
我想解释一下为什么我首先使用 order with limit:我计划以特定的概率从 table 中随机获取记录。从 python 生成的随机索引并作为变量发送到 MySQL。但后来我发现它花费了太多时间,所以我决定只使用我得到的第一条记录。
在O. Jones和Vijay的启发下,我尝试使用max函数,但效果不佳:
select max(ItemComments) from ItemInfo where ItemType='item_type'
/* Affected rows: 0 Found rows: 1 Warnings: 0 Duration for 1 query: 6.225 sec. */
explain result:
+----+-------------+------------+------------+------+---------------+----------+---------+-------+-------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+------+---------------+----------+---------+-------+-------+----------+-------+
| 1 | SIMPLE | ItemInfo | NULL | ref | ItemType | ItemType | 767 | const | 27378 | 100.00 | NULL |
+----+-------------+------------+------------+------+---------------+----------+---------+-------+-------+----------+-------+
感谢大家对这个问题的贡献。希望大家能根据以上信息提出更多的解决方案。
您的第一个查询排序升序可以利用您在 ItemComment
上的索引。
SELECT * ... ORDER BY ... LIMIT 1
是臭名昭著的性能反模式。为什么?服务器必须对一堆乱七八糟的行进行排序,只丢弃除第一行以外的所有行。
您可以试试这个(对于您的降序变体)。它有点冗长,但效率更高。
SELECT a.*
FROM ItemInfo a
JOIN (
SELECT MAX(ItemComments) ItemComments, ItemType
FROM ItemInfo
GROUP BY ItemType
) maxcomm ON a.ItemType = maxcomm.ItemType
AND a.ItemComments = maxcomm.ItemComments
WHERE a.ItemType = 'item type'
为什么这行得通?它使用 GROUP BY / MAX()
而不是 ORDER BY ... DESC LIMIT 1
来查找最大值。子查询执行您的搜索。
为了使这项工作尽可能高效,您需要 (ItemType, ItemComments)
上的复合(多列)索引。使用
创建
ALTER TABLE ItemInfo CREATE INDEX ItemTypeCommentIndex (ItemType, ItemComments);
创建新索引时,将索引放在 ItemType
上,因为新索引与那个索引是多余的。
MySQL 的查询规划器足够聪明,可以在运行内部 GROUP BY
查询之前看到外部 WHERE
子句,因此它不必聚合整个 table.
有了复合索引MySQL可以使用松散索引扫描来满足子查询。这些几乎快得惊人。您应该仔细阅读该主题。
您的查询将 select 基于 where 条件的所有行。之后它将根据 order by statement 对行进行排序,然后它将 select 第一行。更好的查询类似于
SELECT ifo.* FROM ItemInfo ifo
JOIN (SELECT MAX(ItemComments) AS max_comnt FROM ItemInfo WHERE ItemType="item_type") inn_ifo
ON ifo.ItemComments = inn_ifo.max_comnt
因为这个查询只从列中找到最大值。寻找 MAX() 只是 O(n) 但最快的排序算法是 O(nlogn) 。因此,如果您避免 order by statemet,查询将执行得更快。
希望这对您有所帮助。
请提供当前 SHOW CREATE TABLE ItemInfo
。
对于大多数查询,您需要复合索引
INDEX(ItemType, ItemComments)
最后一个,你需要
INDEX(ItemComments)
对于特别慢的查询,请提供EXPLAIN SELECT ...
。
讨论 - 为什么 INDEX(ItemType, ItemComments)
对 where ItemType="item_type" order by ItemComments desc limit 1
有帮助?
索引在 BTree
中构建(参见维基百科),因此可以非常快速地搜索单个项目,并且可以非常快速地按特定顺序进行扫描。
where ItemType="item_type"
说要在 ItemType
上过滤,但索引中有很多这样的。在此索引中,它们按 ItemComments
排序(对于给定的 ItemType
)。方向desc
建议从ItemContents
的最高值开始;那就是索引项的'end'。最后 limit 1
表示找到一项后停止。 (有点像在你的 Rolodex 中找到最后一个 "S"。)
因此查询是 'drill down' BTree 到复合 INDEX(ItemType, ItemContents)
中 ItemType
条目的末尾并获取一个条目——一项非常高效的任务。
实际上SELECT *
意味着还有一个步骤,即获取该行的所有列。该信息不在索引中,但在 ItemInfo
的 BTree 中——它包含所有行的所有列,按 PRIMARY KEY
.
排序
"secondary index" (INDEX(ItemType, ItemComments)
) 隐式包含相关 PRIMARY KEY
列的副本,因此我们现在有 ItemID
和 ItemType
的值.有了这些,我们可以向下钻取另一个 BTree 以找到所需的行并获取所有 (*
) 列。
我有这个 select 查询,ItemType 是 varchar 类型,ItemComments 是 int 类型:
select * from ItemInfo where ItemType="item_type" order by ItemComments desc limit 1
可以看到这个查询有3个条件:
- 其中 'ItemType' 等于特定值;
- 按 'ItemComments' 排序
- 按降序排列
有趣的是,当我 select 具有所有三个条件的行时,它变得非常慢。但是如果我放弃这三个中的任何一个(条件 2 除外),查询运行得非常快。参见:
select * from ItemInfo where ItemType="item_type" order by ItemComments desc limit 1;
/* Affected rows: 0 Found rows: 1 Warnings: 0 Duration for 1 query: 16.318 sec. */
select * from ItemInfo where ItemType="item_type" order by ItemComments limit 1;
/* Affected rows: 0 Found rows: 1 Warnings: 0 Duration for 1 query: 0.140 sec. */
select * from ItemInfo order by ItemComments desc limit 1;
/* Affected rows: 0 Found rows: 1 Warnings: 0 Duration for 1 query: 0.015 sec. */
此外,
- 我正在使用 MySQL 5.7 和 InnoDB 引擎。
- 我已经在 ItemType 和 ItemComments 上创建了索引,table ItemInfo 包含 200 万行。
我搜索了很多可能的解释,比如MySQL支持降序索引、复合索引等等。但是这些仍然不能解释为什么查询 #1 运行缓慢而查询 #2 和 #3 运行良好。
如果有人能帮助我,我将不胜感激。
更新:创建table并解释信息
创建代码:
CREATE TABLE `ItemInfo` (
`ItemID` VARCHAR(255) NOT NULL,
`ItemType` VARCHAR(255) NOT NULL,
`ItemPics` VARCHAR(255) NULL DEFAULT '0',
`ItemName` VARCHAR(255) NULL DEFAULT '0',
`ItemComments` INT(50) NULL DEFAULT '0',
`ItemScore` DECIMAL(10,1) NULL DEFAULT '0.0',
`ItemPrice` DECIMAL(20,2) NULL DEFAULT '0.00',
`ItemDate` DATETIME NULL DEFAULT '1971-01-01 00:00:00',
PRIMARY KEY (`ItemID`, `ItemType`),
INDEX `ItemDate` (`ItemDate`),
INDEX `ItemComments` (`ItemComments`),
INDEX `ItemType` (`ItemType`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
解释结果:
mysql> explain select * from ItemInfo where ItemType="item_type" order by ItemComments desc limit 1;
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+
| 1 | SIMPLE | i | NULL | index | ItemType | ItemComments | 5 | NULL | 83 | 1.20 | Using where |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+
mysql> explain select * from ItemInfo where ItemType="item_type" order by ItemComments limit 1;
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+
| 1 | SIMPLE | i | NULL | index | ItemType | ItemComments | 5 | NULL | 83 | 1.20 | Using where |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------------+
mysql> explain select * from ItemInfo order by ItemComments desc limit 1;
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------+
| 1 | SIMPLE | i | NULL | index | NULL | ItemComments | 5 | NULL | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-------+
来自 O. Jones 的查询:
mysql> explain
-> SELECT a.*
-> FROM ItemInfo a
-> JOIN (
-> SELECT MAX(ItemComments) ItemComments, ItemType
-> FROM ItemInfo
-> GROUP BY ItemType
-> ) maxcomm ON a.ItemType = maxcomm.ItemType
-> AND a.ItemComments = maxcomm.ItemComments
-> WHERE a.ItemType = 'item_type';
+----+-------------+------------+------------+-------+----------------------------------------+-------------+---------+---------------------------+---------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+-------+----------------------------------------+-------------+---------+---------------------------+---------+----------+--------------------------+
| 1 | PRIMARY | a | NULL | ref | ItemComments,ItemType | ItemType | 767 | const | 27378 | 100.00 | Using where |
| 1 | PRIMARY | <derived2> | NULL | ref | <auto_key0> | <auto_key0> | 772 | mydb.a.ItemComments,const | 10 | 100.00 | Using where; Using index |
| 2 | DERIVED | ItemInfo | NULL | index | PRIMARY,ItemDate,ItemComments,ItemType | ItemType | 767 | NULL | 2289466 | 100.00 | NULL |
+----+-------------+------------+------------+-------+----------------------------------------+-------------+---------+---------------------------+---------+----------+--------------------------+
我不确定我是否正确执行了这个查询,但是我在很长一段时间内无法获取记录。
来自 Vijay 的查询。但是我添加了 ItemType 连接条件,因为只有 max_comnt return 个来自其他 ItemType 的项目:
SELECT ifo.* FROM ItemInfo ifo
JOIN (SELECT ItemType, MAX(ItemComments) AS max_comnt FROM ItemInfo WHERE ItemType="item_type") inn_ifo
ON ifo.ItemComments = inn_ifo.max_comnt and ifo.ItemType = inn_ifo.ItemType
/* Affected rows: 0 Found rows: 1 Warnings: 0 Duration for 1 query: 7.441 sec. */
explain result:
+----+-------------+------------+------------+-------------+-----------------------+-----------------------+---------+-------+-------+----------+-----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+-------------+-----------------------+-----------------------+---------+-------+-------+----------+-----------------------------------------------------+
| 1 | PRIMARY | <derived2> | NULL | system | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 1 | PRIMARY | ifo | NULL | index_merge | ItemComments,ItemType | ItemComments,ItemType | 5,767 | NULL | 88 | 100.00 | Using intersect(ItemComments,ItemType); Using where |
| 2 | DERIVED | ItemInfo | NULL | ref | ItemType | ItemType | 767 | const | 27378 | 100.00 | NULL |
+----+-------------+------------+------------+-------------+-----------------------+-----------------------+---------+-------+-------+----------+-----------------------------------------------------+
我想解释一下为什么我首先使用 order with limit:我计划以特定的概率从 table 中随机获取记录。从 python 生成的随机索引并作为变量发送到 MySQL。但后来我发现它花费了太多时间,所以我决定只使用我得到的第一条记录。
在O. Jones和Vijay的启发下,我尝试使用max函数,但效果不佳:
select max(ItemComments) from ItemInfo where ItemType='item_type'
/* Affected rows: 0 Found rows: 1 Warnings: 0 Duration for 1 query: 6.225 sec. */
explain result:
+----+-------------+------------+------------+------+---------------+----------+---------+-------+-------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+------+---------------+----------+---------+-------+-------+----------+-------+
| 1 | SIMPLE | ItemInfo | NULL | ref | ItemType | ItemType | 767 | const | 27378 | 100.00 | NULL |
+----+-------------+------------+------------+------+---------------+----------+---------+-------+-------+----------+-------+
感谢大家对这个问题的贡献。希望大家能根据以上信息提出更多的解决方案。
您的第一个查询排序升序可以利用您在 ItemComment
上的索引。
SELECT * ... ORDER BY ... LIMIT 1
是臭名昭著的性能反模式。为什么?服务器必须对一堆乱七八糟的行进行排序,只丢弃除第一行以外的所有行。
您可以试试这个(对于您的降序变体)。它有点冗长,但效率更高。
SELECT a.*
FROM ItemInfo a
JOIN (
SELECT MAX(ItemComments) ItemComments, ItemType
FROM ItemInfo
GROUP BY ItemType
) maxcomm ON a.ItemType = maxcomm.ItemType
AND a.ItemComments = maxcomm.ItemComments
WHERE a.ItemType = 'item type'
为什么这行得通?它使用 GROUP BY / MAX()
而不是 ORDER BY ... DESC LIMIT 1
来查找最大值。子查询执行您的搜索。
为了使这项工作尽可能高效,您需要 (ItemType, ItemComments)
上的复合(多列)索引。使用
ALTER TABLE ItemInfo CREATE INDEX ItemTypeCommentIndex (ItemType, ItemComments);
创建新索引时,将索引放在 ItemType
上,因为新索引与那个索引是多余的。
MySQL 的查询规划器足够聪明,可以在运行内部 GROUP BY
查询之前看到外部 WHERE
子句,因此它不必聚合整个 table.
有了复合索引MySQL可以使用松散索引扫描来满足子查询。这些几乎快得惊人。您应该仔细阅读该主题。
您的查询将 select 基于 where 条件的所有行。之后它将根据 order by statement 对行进行排序,然后它将 select 第一行。更好的查询类似于
SELECT ifo.* FROM ItemInfo ifo
JOIN (SELECT MAX(ItemComments) AS max_comnt FROM ItemInfo WHERE ItemType="item_type") inn_ifo
ON ifo.ItemComments = inn_ifo.max_comnt
因为这个查询只从列中找到最大值。寻找 MAX() 只是 O(n) 但最快的排序算法是 O(nlogn) 。因此,如果您避免 order by statemet,查询将执行得更快。 希望这对您有所帮助。
请提供当前 SHOW CREATE TABLE ItemInfo
。
对于大多数查询,您需要复合索引
INDEX(ItemType, ItemComments)
最后一个,你需要
INDEX(ItemComments)
对于特别慢的查询,请提供EXPLAIN SELECT ...
。
讨论 - 为什么 INDEX(ItemType, ItemComments)
对 where ItemType="item_type" order by ItemComments desc limit 1
有帮助?
索引在 BTree
中构建(参见维基百科),因此可以非常快速地搜索单个项目,并且可以非常快速地按特定顺序进行扫描。
where ItemType="item_type"
说要在 ItemType
上过滤,但索引中有很多这样的。在此索引中,它们按 ItemComments
排序(对于给定的 ItemType
)。方向desc
建议从ItemContents
的最高值开始;那就是索引项的'end'。最后 limit 1
表示找到一项后停止。 (有点像在你的 Rolodex 中找到最后一个 "S"。)
因此查询是 'drill down' BTree 到复合 INDEX(ItemType, ItemContents)
中 ItemType
条目的末尾并获取一个条目——一项非常高效的任务。
实际上SELECT *
意味着还有一个步骤,即获取该行的所有列。该信息不在索引中,但在 ItemInfo
的 BTree 中——它包含所有行的所有列,按 PRIMARY KEY
.
"secondary index" (INDEX(ItemType, ItemComments)
) 隐式包含相关 PRIMARY KEY
列的副本,因此我们现在有 ItemID
和 ItemType
的值.有了这些,我们可以向下钻取另一个 BTree 以找到所需的行并获取所有 (*
) 列。