我在 MySQL 中运行这个查询:
SELECT
count(*)
FROM
library AS l
JOIN plays AS p ON p.user_id = l.user_id AND
l.path = p.path
WHERE
l.user_id = 20977 AND
p.time >= '2022-10-17';
运行 EXPLAIN ANALYZE 时:
| -> Aggregate: count(0) (cost=1085653.55 rows=6692) (actual time=12576.265..12576.266 rows=1 loops=1)
-> Nested loop inner join (cost=1084984.37 rows=6692) (actual time=40.604..12566.569 rows=56757 loops=1)
-> Index lookup on l using user_id_2 (user_id=20977) (cost=116747.95 rows=106784) (actual time=13.153..3783.204 rows=59631 loops=1)
-> Filter: ((p.user_id = 20977) and (p.`time` >= TIMESTAMP'2022-10-17 00:00:00')) (cost=8.24 rows=0) (actual time=0.135..0.147 rows=1 loops=59631)
-> Index lookup on p using path (path=l.`path`) (cost=8.24 rows=8) (actual time=0.090..0.146 rows=1 loops=59631)
|
1 row in set (12.76 sec)
我显然想让这个更快!
CREATE TABLE `library` (
`user_id` int NOT NULL,
`name` varchar(20) COLLATE utf8mb4_general_ci NOT NULL,
`path` varchar(512) COLLATE utf8mb4_general_ci NOT NULL,
`title` varchar(512) COLLATE utf8mb4_general_ci NOT NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`edited` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`db_id` int NOT NULL,
`tag` varchar(64) COLLATE utf8mb4_general_ci NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `plays` (
`user_id` int DEFAULT NULL,
`name` varchar(20) CHARACTER SET utf8 DEFAULT NULL,
`path` varchar(512) COLLATE utf8mb4_general_ci DEFAULT NULL,
`time` datetime DEFAULT CURRENT_TIMESTAMP,
`play_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
ALTER TABLE `library`
ADD PRIMARY KEY (`db_id`),
ADD KEY `user_id_loc` (`user_id`,`name`,`path`(191)),
ADD KEY `edited` (`edited`),
ADD KEY `created` (`created`),
ADD KEY `title` (`title`),
ADD KEY `user_id` (`user_id`),
ADD INDEX `user_id_by_title` (`user_id`, `title`);
ALTER TABLE `plays`
ADD PRIMARY KEY (`play_id`),
ADD KEY `user_id` (`user_id`,`name`,`path`(255)),
ADD KEY `user_id_2` (`user_id`,`name`),
ADD KEY `time` (`time`),
ADD KEY `path` (`path`),
ADD KEY `user_id_3` (`user_id`,`name`,`path`,`time`);
看起来杀手是循环 59631 行。
(user_id, time)
上的索引会让速度更快吗?
有趣的是,
user_id_2
索引实际上是(user_id, title)
上的索引,而不是普通的user_id
索引。我不确定为什么在查询中未使用 user_id_2
时选择 title
。
我测试了您的查询并在每个表中尝试了不同的索引。
ALTER TABLE library ADD KEY bk1 (user_id, path);
ALTER TABLE plays ADD KEY bk2 (user_id, path, time);
EXPLAIN SELECT
COUNT(*)
FROM
library AS l USE INDEX (bk1)
JOIN plays AS p USE INDEX (bk2)
ON p.user_id = l.user_id
AND l.path = p.path
WHERE
l.user_id = 20977
AND p.time >= '2022-10-17';
+----+-------------+-------+------------+------+---------------+------+---------+-------------------+------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+-------------------+------+----------+--------------------------+
| 1 | SIMPLE | l | NULL | ref | bk1 | bk1 | 4 | const | 1 | 100.00 | Using index |
| 1 | SIMPLE | p | NULL | ref | bk2 | bk2 | 2056 | const,test.l.path | 1 | 100.00 | Using where; Using index |
+----+-------------+-------+------------+------+---------------+------+---------+-------------------+------+----------+--------------------------+
EXPLAIN 报告每一行中的注释“使用索引”表明它从两个表的覆盖索引中受益。
我没有使用前缀索引语法,因为这会破坏覆盖索引优化。在现代 MySQL 版本上,没有必要使用此示例的前缀索引,因为它们默认为支持 3072 字节索引的 InnoDB 行格式,而不是默认情况下仅支持 768 字节索引的旧 MySQL。
在我的测试中,我测试的表中有零行,因此我必须使用索引提示来使优化器选择我的新索引。在具有大量行的表中,优化器可能会自行选择新索引。
删除这些,它们妨碍和/或多余:
l: `user_id` (`user_id`),
p: `user_id` (`user_id`,`name`,`path`(255)),
p: `user_id_2` (`user_id`,`name`),
添加这些:
l: INDEX(user_id, path)
p: INDEX(user_id, path, time)
p: INDEX(user_id, time, path) -- see below
更改(MySQL 5.7/8.0 不再需要前缀 kludge):
l: `user_id_loc` (`user_id`,`name`,`path`) -- tossing 191
尽量避免测试
WHERE
中不同表的列。
我第一次看到
WHERE l.user_id = 20977
AND p.time >= '2022-10-17';
并认为这就是问题的症结所在。但后来我发现您在
INDEX(user_id, time)
上没有
p
并且 表格在 user_id
上[部分]连接。
建议(以避免我的困惑)您进行此更改:
WHERE l.user_id = 20977 -- >
WHERE p.user_id = 20977
优化器应该足够聪明,能够意识到这一点,然后使用
p: INDEX(user_id, time, path) -- as mentioned above
但是,一旦完成此操作,查询就会折叠为
SELECT COUNT(DISTINCT user_id, path)
FROM plays
WHERE user_id = 20977
AND time >= '2022-10-17';
但是,如果存在“库”中没有相应条目的“游戏”,则计数将按缺失的用户玩组合的数量而增加。