带有索引查找和过滤器的嵌套循环内连接速度很慢

问题描述 投票:0回答:2

我在 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

sql mysql join sql-execution-plan
2个回答
1
投票

我测试了您的查询并在每个表中尝试了不同的索引。

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。

在我的测试中,我测试的表中有零行,因此我必须使用索引提示来使优化器选择我的新索引。在具有大量行的表中,优化器可能会自行选择新索引。


0
投票

删除这些,它们妨碍和/或多余:

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';

但是,如果存在“库”中没有相应条目的“游戏”,则计数将按缺失的用户玩组合的数量而增加。

© www.soinside.com 2019 - 2024. All rights reserved.