SQL 使用 3 个连接优化查询
SQL query optimization with 3 joins
我正在学习执行时间方面的查询优化行为。我有 3 个 table,分别是帖子、评论和用户。下面的查询通过 返回 2010 年发表评论多于帖子的用户及其计数 来完美运行。我相信它可以被优化,我会很感激一个带有解释的优化代码。
每个 Table
的信息
- 用户table;有 40325 行,在其主键 (Id) 上有索引。
- 帖子table;有 91986 行,在其主索引上
key(PostId) 和 OwnerUserId,它是外键
引用用户 table。
- 评论 table - 有 174305 行,索引在其主键上,UserId(对于用户 table)和 PostId(对于帖子 table)。
SELECT pos_table.user_ID, pos_table.Username, comms, pos from
(SELECT
users.Id as 'user_ID', users.DisplayName as 'Username', count(posts.Id) as pos
FROM
users
INNER JOIN posts ON posts.OwnerUserId = users.Id
WHERE YEAR(posts.CreationDate) = 2010
group by users.Id
) pos_table
JOIN
(SELECT
users.Id as 'user_ID', users.DisplayName as 'Username', count(comments.Id) as
comms
FROM
users
INNER JOIN comments ON comments.UserId = users.Id
WHERE YEAR(comments.CreationDate) = 2010
group by users.Id
) comms_table
on pos_table.user_ID = comms_table.user_ID
HAVING comms > pos
order by user_ID
limit 50;
在我上面的查询中,有两个子查询。一个用于帖子,另一个用于评论。我想看看如何优化它以缩短执行时间。
上述查询的结果,以及我的 EXPLAIN 查询的附件:
有一件事让我印象深刻。您的两个子查询中都有这种行。
WHERE YEAR(posts.CreationDate) = 2010
您对列值调用函数。那不是 sargeable。它可以防止 MySQL 能够利用该列上的索引,而是需要进行全面扫描。 (MySQL 和其他 DBMS 仍然愚蠢到不知道 YEAR(timestamp)
可以通过索引范围扫描来满足。)
所以把那些WHERE改成这种东西。
WHERE posts.CreationDate >= '2010-01-01'
AND posts.CreationDate < '2010-01-01' + INTERVAL 1 YEAR
并在您的帖子和评论 table 中的 CreationDate 列中建立索引。然后查询规划器可以在索引中随机查找到第一个匹配行,然后顺序读取它直到最后一个匹配行。这称为索引范围扫描,比完整 table 扫描更有效。
EDIT 您需要以下索引:
CREATE INDEX date_user ON posts ( CreationDate, OwnerUserId );
CREATE INDEX date_user ON comments ( CreationDate, UserID);
我建议您重构查询以使所有工作发生的子查询更快。
这些应该是子查询。在所需的时间范围内,它们各自为每个用户生成一些项目。
SELECT OwnerUserId, COUNT(*) posts
FROM posts
WHERE CreationDate >= '2010-01-01'
AND CreationDate < '2010-01-01' + INTERVAL 1 YEAR
GROUP BY OwnerUserId
SELECT UserId, COUNT(*) comments
FROM comments
WHERE CreationDate >= '2010-01-01'
AND CreationDate < '2010-01-01' + INTERVAL 1 YEAR
GROUP BY UserId
这些查询通过仅聚合(按组汇总)满足查询所需的最少数据量来节省时间。而且,他们可以通过对我建议的索引进行快速索引范围扫描来满足。
然后,您可以在主查询中使用这些子查询,从您的 users
table 中获取用户名,就像这样。
SELECT users.Id user_ID, users.Username, c.comments, p.posts
FROM users
JOIN (
SELECT OwnerUserId, COUNT(*) posts
FROM posts
WHERE CreationDate >= '2010-01-01'
AND CreationDate < '2010-01-01' + INTERVAL 1 YEAR
GROUP BY OwnerUserId
) p ON users.ID = p.OwnerUserId
JOIN (
SELECT UserId, COUNT(*) comments
FROM comments
WHERE CreationDate >= '2010-01-01'
AND CreationDate < '2010-01-01' + INTERVAL 1 YEAR
GROUP BY UserId
) c ON users.ID = c.UserId
WHERE c.comments > p.posts
ORDER BY users.ID
LIMIT 50;
我怀疑如果添加我提到的复合索引,您的性能会得到很大提升。您可以删除 CreationDate 上的索引;当您添加复合索引时,它们是多余的。
这里有一个值得参考的地方https://use-the-index-luke.com/
我正在学习执行时间方面的查询优化行为。我有 3 个 table,分别是帖子、评论和用户。下面的查询通过 返回 2010 年发表评论多于帖子的用户及其计数 来完美运行。我相信它可以被优化,我会很感激一个带有解释的优化代码。
每个 Table
的信息- 用户table;有 40325 行,在其主键 (Id) 上有索引。
- 帖子table;有 91986 行,在其主索引上 key(PostId) 和 OwnerUserId,它是外键 引用用户 table。
- 评论 table - 有 174305 行,索引在其主键上,UserId(对于用户 table)和 PostId(对于帖子 table)。
SELECT pos_table.user_ID, pos_table.Username, comms, pos from
(SELECT
users.Id as 'user_ID', users.DisplayName as 'Username', count(posts.Id) as pos
FROM
users
INNER JOIN posts ON posts.OwnerUserId = users.Id
WHERE YEAR(posts.CreationDate) = 2010
group by users.Id
) pos_table
JOIN
(SELECT
users.Id as 'user_ID', users.DisplayName as 'Username', count(comments.Id) as
comms
FROM
users
INNER JOIN comments ON comments.UserId = users.Id
WHERE YEAR(comments.CreationDate) = 2010
group by users.Id
) comms_table
on pos_table.user_ID = comms_table.user_ID
HAVING comms > pos
order by user_ID
limit 50;
在我上面的查询中,有两个子查询。一个用于帖子,另一个用于评论。我想看看如何优化它以缩短执行时间。
上述查询的结果,以及我的 EXPLAIN 查询的附件:
有一件事让我印象深刻。您的两个子查询中都有这种行。
WHERE YEAR(posts.CreationDate) = 2010
您对列值调用函数。那不是 sargeable。它可以防止 MySQL 能够利用该列上的索引,而是需要进行全面扫描。 (MySQL 和其他 DBMS 仍然愚蠢到不知道 YEAR(timestamp)
可以通过索引范围扫描来满足。)
所以把那些WHERE改成这种东西。
WHERE posts.CreationDate >= '2010-01-01'
AND posts.CreationDate < '2010-01-01' + INTERVAL 1 YEAR
并在您的帖子和评论 table 中的 CreationDate 列中建立索引。然后查询规划器可以在索引中随机查找到第一个匹配行,然后顺序读取它直到最后一个匹配行。这称为索引范围扫描,比完整 table 扫描更有效。
EDIT 您需要以下索引:
CREATE INDEX date_user ON posts ( CreationDate, OwnerUserId );
CREATE INDEX date_user ON comments ( CreationDate, UserID);
我建议您重构查询以使所有工作发生的子查询更快。
这些应该是子查询。在所需的时间范围内,它们各自为每个用户生成一些项目。
SELECT OwnerUserId, COUNT(*) posts
FROM posts
WHERE CreationDate >= '2010-01-01'
AND CreationDate < '2010-01-01' + INTERVAL 1 YEAR
GROUP BY OwnerUserId
SELECT UserId, COUNT(*) comments
FROM comments
WHERE CreationDate >= '2010-01-01'
AND CreationDate < '2010-01-01' + INTERVAL 1 YEAR
GROUP BY UserId
这些查询通过仅聚合(按组汇总)满足查询所需的最少数据量来节省时间。而且,他们可以通过对我建议的索引进行快速索引范围扫描来满足。
然后,您可以在主查询中使用这些子查询,从您的 users
table 中获取用户名,就像这样。
SELECT users.Id user_ID, users.Username, c.comments, p.posts
FROM users
JOIN (
SELECT OwnerUserId, COUNT(*) posts
FROM posts
WHERE CreationDate >= '2010-01-01'
AND CreationDate < '2010-01-01' + INTERVAL 1 YEAR
GROUP BY OwnerUserId
) p ON users.ID = p.OwnerUserId
JOIN (
SELECT UserId, COUNT(*) comments
FROM comments
WHERE CreationDate >= '2010-01-01'
AND CreationDate < '2010-01-01' + INTERVAL 1 YEAR
GROUP BY UserId
) c ON users.ID = c.UserId
WHERE c.comments > p.posts
ORDER BY users.ID
LIMIT 50;
我怀疑如果添加我提到的复合索引,您的性能会得到很大提升。您可以删除 CreationDate 上的索引;当您添加复合索引时,它们是多余的。
这里有一个值得参考的地方https://use-the-index-luke.com/