我有一个包含以下列的表格:
记录ID
来源 ID
用户 ID
移动
叫_at
我正在尝试运行这两个查询
SELECT
t1.user_id,
t1.mobile,
COUNT(DISTINCT(t1.called_at )) AS cnt
FROM
(
SELECT
user_id,
mobile,
called_at
FROM
users
WHERE
called_at >= "2016-09-01" AND called_at < "2016-12-01" and user_id is NOT NULL
) t1
GROUP BY t1.user_id, t1.mobile
HAVING cnt > 1
还有
SELECT
user_id,
mobile,
COUNT(DISTINCT(called_at )) AS cnt
FROM users
WHERE called_at >= "2016-09-01" AND called_at < "2016-12-01" and user_id is NOT NULL
GROUP BY user_id, mobile
HAVING cnt > 1
两个查询在逻辑上是相同的,并且也给出相同的输出。但第一个查询运行得非常快 ~ 3 秒,第二个查询 ~ 55 秒。
甚至解释说第一个查询涉及使用文件排序对派生表进行额外扫描,但速度仍然快得多。
这怎么可能?
解释输出:
+----+-------------+-----------------------+------+-----------------------+------+---------+------+---------+----------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-----------------------+------+-----------------------+------+---------+------+---------+----------------+
| 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 1025150 | Using filesort |
| 2 | DERIVED | users | ALL | idx_fa_af,idx_a_di_um | NULL | NULL | NULL | 2221923 | Using where |
+----+-------------+-----------------------+------+-----------------------+------+---------+------+---------+----------------+
+----+-------------+-----------------------+-------+-----------------------+-------------+---------+------+---------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-----------------------+-------+-----------------------+-------------+---------+------+---------+-------------+
| 1 | SIMPLE | users | index | idx_fa_af,idx_a_di_um | idx_a_di_um | 23 | NULL | 2221923 | Using where |
+----+-------------+-----------------------+-------+-----------------------+-------------+---------+------+---------+-------------+
| users | CREATE TABLE `users` (
`record_id` varchar(100) NOT NULL,
`source_id` int(11) NOT NULL,
`user_id` int(11) DEFAULT NULL,
`mobile` varchar(15) DEFAULT NULL,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`called_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
UNIQUE KEY `idx_unique_a_ri_si` (`record_id`,`source_id`),
KEY `idx_fa_af` (`called_at`),
KEY `idx_fa_um` (`mobile`),
KEY `idx_a_di_um` (`user_id`,`mobile`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+----------------------------+---------+
| Variable_name | Value |
+----------------------------+---------+
| Handler_commit | 1 |
| Handler_delete | 0 |
| Handler_discover | 0 |
| Handler_external_lock | 2 |
| Handler_mrr_init | 0 |
| Handler_prepare | 0 |
| Handler_read_first | 1 |
| Handler_read_key | 1 |
| Handler_read_last | 0 |
| Handler_read_next | 0 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 3676447 |
| Handler_rollback | 0 |
| Handler_savepoint | 0 |
| Handler_savepoint_rollback | 0 |
| Handler_update | 0 |
| Handler_write | 1208173 |
+----------------------------+---------+
+----------------------------+---------+
| Variable_name | Value |
+----------------------------+---------+
| Handler_commit | 1 |
| Handler_delete | 0 |
| Handler_discover | 0 |
| Handler_external_lock | 2 |
| Handler_mrr_init | 0 |
| Handler_prepare | 0 |
| Handler_read_first | 1 |
| Handler_read_key | 1 |
| Handler_read_last | 0 |
| Handler_read_next | 2468272 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 0 |
| Handler_rollback | 0 |
| Handler_savepoint | 0 |
| Handler_savepoint_rollback | 0 |
| Handler_update | 0 |
| Handler_write | 0 |
+----------------------------+---------+
最佳答案
添加INDEX(user_id, Called_at, mobile)
,然后运行每个查询两次。两次是为了避免可能隐藏 I/O 的缓存问题。
我怀疑第一个查询运行得很快,因为它全部在 RAM 中。第二个是使用未缓存的索引 idx_a_di_um。
我建议的索引应该使它们都运行得更快。
任何列的组合是否“唯一”?如果是这样,请将组合设为主键
。这将进一步改善情况。如果没有,至少提供一个 id INT UNSIGNED AUTO_INCRMENT NOT NULL PRIMARY KEY
。
为什么会有帮助
索引是一个 BTree。 (有关详细定义,请参阅维基百科。)该索引结构与数据分开,数据位于单独的 BTree 中,按 PRIMARY KEY 排序。 BTree 在查找一行或一组连续行方面非常高效。 (根据索引“连续”。)当使用辅助键(即不是 PRIMARY
)时,首先定位索引的行,然后定位每个数据 使用PRIMARY KEY
查找行。除非...如果 SELECT
中所需的所有列都在辅助键中,则无需访问数据。这称为“覆盖”; EXPLAIN
通过“使用索引”来表示。我的索引是子查询的“覆盖”索引。
任何索引中列的顺序都很重要。在这种情况下,索引将所有 user_id IS NOT NULL
行放在一起。但这是关于 3 列顺序的唯一论据。
处理程序技巧
这里有一种方法可以更深入地了解查询正在做什么,它不依赖于缓存、服务器重新启动等:
FLUSH STATUS;
SELECT ...;
SHOW SESSION STATUS LIKE 'Handler%';
看起来像表大小(行)的数字表示表(或索引)扫描。看起来像输出大小的数字表示一些最终的操作。 Handler_write...表示临时表。等等
关于Mysql 派生表性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41147136/