非常慢 MySQL 查询性能
Very slow MySQL query performance
我的查询大约需要 18 秒 才能完成:
查询:
SELECT YEAR(c.date), MONTH(c.date), p.district_id, COUNT(p.owner_id)
FROM commission c
INNER JOIN partner p ON c.customer_id = p.id
WHERE (c.date BETWEEN '2018-01-01' AND '2018-12-31')
AND (c.company_id = 90)
AND (c.source = 'ACTUAL')
AND (p.id IN (3062, 3063, 3064, 3065, 3066, 3067, 3068, 3069, 3070, 3071,
3072, 3073, 3074, 3075, 3076, 3077, 3078, 3079, 3081, 3082, 3083, 3084,
3085, 3086, 3087, 3088, 3089, 3090, 3091, 3092, 3093, 3094, 3095, 3096,
3097, 3098, 3099, 3448, 3449, 3450, 3451, 3452, 3453, 3454, 3455, 3456,
3457, 3458, 3459, 3460, 3461, 3471, 3490, 3491, 6307, 6368, 6421))
GROUP BY YEAR(c.date), MONTH(c.date), p.district_id
commission
table 有大约 280 万 条记录,其中 860 000+属于当前的 2018 年。partner
table 目前有 8600 多条记录。
结果
| `YEAR(c.date)` | `MONTH(c.date)` | district_id | `COUNT(c.id)` |
|----------------|-----------------|-------------|---------------|
| 2018 | 1 | 1 | 19154 |
| 2018 | 1 | 5 | 9184 |
| 2018 | 1 | 6 | 2706 |
| 2018 | 1 | 12 | 36296 |
| 2018 | 1 | 15 | 13085 |
| 2018 | 2 | 1 | 21231 |
| 2018 | 2 | 5 | 10242 |
| ... | ... | ... | ... |
55 rows retrieved starting from 1 in 18 s 374 ms
(execution: 18 s 368 ms, fetching: 6 ms)
解释:
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | extra |
|----|-------------|-------|------------|-------|------------------------------------------------------------------------------------------------------|----------------------|---------|-----------------|------|----------|----------------------------------------------|
| 1 | SIMPLE | p | null | range | PRIMARY | PRIMARY | 4 | | 57 | 100 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | c | null | ref | UNIQ_6F7146F0979B1AD62FC0CB0F5F8A7F73,IDX_6F7146F09395C3F3,IDX_6F7146F0979B1AD6,IDX_6F7146F0AA9E377A | IDX_6F7146F09395C3F3 | 5 | p.id | 6716 | 8.33 | Using where |
DDL:
create table if not exists commission (
id int auto_increment
primary key,
date date not null,
source enum('ACTUAL', 'EXPECTED') not null,
customer_id int null,
transaction_id varchar(255) not null,
company_id int null,
constraint UNIQ_6F7146F0979B1AD62FC0CB0F5F8A7F73 unique (company_id, transaction_id, source),
constraint FK_6F7146F09395C3F3 foreign key (customer_id) references partner (id),
constraint FK_6F7146F0979B1AD6 foreign key (company_id) references companies (id)
) collate=utf8_unicode_ci;
create index IDX_6F7146F09395C3F3 on commission (customer_id);
create index IDX_6F7146F0979B1AD6 on commission (company_id);
create index IDX_6F7146F0AA9E377A on commission (date);
我注意到通过删除伙伴 IN
条件 MySQL 只需要 3 秒。我试图用这样疯狂的方式替换它:
AND (',3062,3063,3064,3065,3066,3067,3068,3069,3070,3071,3072,3073,3074,3075,3076,3077,3078,3079,3081,3082,3083,3084,3085,3086,3087,3088,3089,3090,3091,3092,3093,3094,3095,3096,3097,3098,3099,3448,3449,3450,3451,3452,3453,3454,3455,3456,3457,3458,3459,3460,3461,3471,3490,3491,6307,6368,6421,'
LIKE CONCAT('%,', p.id, ',%'))
结果大约是 5 秒...太棒了!但这是一个黑客。
为什么当我使用 IN
语句时, 这个查询的执行时间很长?解决方法、提示、链接等。谢谢!
MySQL 一次可以使用一个索引。对于此查询,您需要一个涵盖搜索各个方面的复合索引。 WHERE 子句的常量方面应该在范围方面之前使用,例如:
ALTER TABLE commission
DROP INDEX IDX_6F7146F0979B1AD6,
ADD INDEX IDX_6F7146F0979B1AD6 (company_id, source, date)
你的 LIKE-hack 是在欺骗优化器,所以它使用不同的计划(很可能首先使用 IDX_6F7146F0AA9E377A 索引)。
你应该能在解释中看到这个。
我认为你的情况的真正问题是解释的第二行:服务器对 6716 行执行多个函数(MONTH,YEAR),然后尝试对所有这些行进行分组。在此期间,应存储所有这 6716 行(根据您的服务器配置存储在内存或磁盘上)。
SELECT COUNT(*) FROM commission WHERE (date BETWEEN '2018-01-01' AND '2018-12-31') AND company_id = 90 AND source = 'ACTUAL';
=> 我们在谈论多少行?
如果上述查询中的数字远低于 6716,我会尝试在列 customer_id、company_id、来源和日期上添加覆盖索引。不确定最佳顺序,因为它取决于您拥有的数据(检查这些列的基数)。我从索引开始(日期,company_id,来源,customer_id)。另外,我会在合作伙伴上添加唯一索引(id,district_id,owner_id)。
也可以添加额外的生成存储列_year和_month(如果你的服务器有点旧你可以添加普通列并用触发器填充它们)以摆脱多重函数执行。
这是优化器在您的查询中看到的内容。
检查GROUP BY
是否使用索引:
- 函数(
YEAR()
)在GROUP BY
,所以没有。
- 提到了多个 table(
c
和 p
),所以没有。
对于 JOIN
,优化器将(几乎总是)从一个开始,然后进入另一个。那么,让我们看看这两个选项:
如果开始 p
:
假设你有 PRIMARY KEY(id)
,没有什么可考虑的。它只会使用该索引。
对于从 p
中选择的每一行,它将查看 c
,并且此 INDEX
的任何变化都是最佳的。
c: INDEX(company_id, source, customer_id, -- in any order (all are tested "=")
date) -- last, since it is tested as a range
如果开始与c
:
c: INDEX(company_id, source, -- in any order (all are tested "=")
date) -- last, since it is tested as a range
-- slightly better:
c: INDEX(company_id, source, -- in any order (all are tested "=")
date, -- last, since it is tested as a range
customer_id) -- really last -- added only to make it "covering".
优化器将查看 "statistics" 以粗略地决定从哪个 table 开始。所以,添加我建议的所有索引。
"covering" 索引包含 所有 查询中任何地方 所需的列。 有时 明智的做法是用更多列扩展 'good' 索引,使其成为 "covering".
但是这里有一个活动扳手。 c.customer_id = p.id
表示 customer_id IN (...)
实际上存在。但是现在有两个 "range-like" 约束——一个是 IN
,另一个是 'range'。在一些较新的版本中,由于 IN
和 仍然 能够进行 "range" 扫描,优化器将愉快地跳来跳去。所以,我推荐这个顺序:
-
column = constant
的测试
- 使用
IN
进行测试
- 一个 'range' 测试(
BETWEEN
、>=
、LIKE
带尾随通配符等)
- 也许添加更多的列以使其成为 "covering" -- 但如果您最终在索引中有超过 5 个列,请不要执行此步骤。
因此,对于c
,以下是WHERE
的最佳选择,恰好是"covering"。
INDEX(company_id, source, -- first, but in any order (all "=")
customer_id, -- "IN"
date) -- last, since it is tested as a range
p: (same as above)
因为有 IN
或 "range",所以看索引是否也可以处理 GROUP BY
没有用。
关于 COUNT(x)
的注释 -- 它检查 x
是否为 NOT NULL
。 通常和COUNT(*)
一样正确,它计算行数而不进行任何额外检查。
这是一个非启动器,因为它在函数中隐藏了索引列 (id
):
AND (',3062,3063,3064,3065,3066,...6368,6421,'
LIKE CONCAT('%,', p.id, ',%'))
我的查询大约需要 18 秒 才能完成:
查询:
SELECT YEAR(c.date), MONTH(c.date), p.district_id, COUNT(p.owner_id)
FROM commission c
INNER JOIN partner p ON c.customer_id = p.id
WHERE (c.date BETWEEN '2018-01-01' AND '2018-12-31')
AND (c.company_id = 90)
AND (c.source = 'ACTUAL')
AND (p.id IN (3062, 3063, 3064, 3065, 3066, 3067, 3068, 3069, 3070, 3071,
3072, 3073, 3074, 3075, 3076, 3077, 3078, 3079, 3081, 3082, 3083, 3084,
3085, 3086, 3087, 3088, 3089, 3090, 3091, 3092, 3093, 3094, 3095, 3096,
3097, 3098, 3099, 3448, 3449, 3450, 3451, 3452, 3453, 3454, 3455, 3456,
3457, 3458, 3459, 3460, 3461, 3471, 3490, 3491, 6307, 6368, 6421))
GROUP BY YEAR(c.date), MONTH(c.date), p.district_id
commission
table 有大约 280 万 条记录,其中 860 000+属于当前的 2018 年。partner
table 目前有 8600 多条记录。
结果
| `YEAR(c.date)` | `MONTH(c.date)` | district_id | `COUNT(c.id)` |
|----------------|-----------------|-------------|---------------|
| 2018 | 1 | 1 | 19154 |
| 2018 | 1 | 5 | 9184 |
| 2018 | 1 | 6 | 2706 |
| 2018 | 1 | 12 | 36296 |
| 2018 | 1 | 15 | 13085 |
| 2018 | 2 | 1 | 21231 |
| 2018 | 2 | 5 | 10242 |
| ... | ... | ... | ... |
55 rows retrieved starting from 1 in 18 s 374 ms
(execution: 18 s 368 ms, fetching: 6 ms)
解释:
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | extra |
|----|-------------|-------|------------|-------|------------------------------------------------------------------------------------------------------|----------------------|---------|-----------------|------|----------|----------------------------------------------|
| 1 | SIMPLE | p | null | range | PRIMARY | PRIMARY | 4 | | 57 | 100 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | c | null | ref | UNIQ_6F7146F0979B1AD62FC0CB0F5F8A7F73,IDX_6F7146F09395C3F3,IDX_6F7146F0979B1AD6,IDX_6F7146F0AA9E377A | IDX_6F7146F09395C3F3 | 5 | p.id | 6716 | 8.33 | Using where |
DDL:
create table if not exists commission (
id int auto_increment
primary key,
date date not null,
source enum('ACTUAL', 'EXPECTED') not null,
customer_id int null,
transaction_id varchar(255) not null,
company_id int null,
constraint UNIQ_6F7146F0979B1AD62FC0CB0F5F8A7F73 unique (company_id, transaction_id, source),
constraint FK_6F7146F09395C3F3 foreign key (customer_id) references partner (id),
constraint FK_6F7146F0979B1AD6 foreign key (company_id) references companies (id)
) collate=utf8_unicode_ci;
create index IDX_6F7146F09395C3F3 on commission (customer_id);
create index IDX_6F7146F0979B1AD6 on commission (company_id);
create index IDX_6F7146F0AA9E377A on commission (date);
我注意到通过删除伙伴 IN
条件 MySQL 只需要 3 秒。我试图用这样疯狂的方式替换它:
AND (',3062,3063,3064,3065,3066,3067,3068,3069,3070,3071,3072,3073,3074,3075,3076,3077,3078,3079,3081,3082,3083,3084,3085,3086,3087,3088,3089,3090,3091,3092,3093,3094,3095,3096,3097,3098,3099,3448,3449,3450,3451,3452,3453,3454,3455,3456,3457,3458,3459,3460,3461,3471,3490,3491,6307,6368,6421,'
LIKE CONCAT('%,', p.id, ',%'))
结果大约是 5 秒...太棒了!但这是一个黑客。
为什么当我使用 IN
语句时, 这个查询的执行时间很长?解决方法、提示、链接等。谢谢!
MySQL 一次可以使用一个索引。对于此查询,您需要一个涵盖搜索各个方面的复合索引。 WHERE 子句的常量方面应该在范围方面之前使用,例如:
ALTER TABLE commission
DROP INDEX IDX_6F7146F0979B1AD6,
ADD INDEX IDX_6F7146F0979B1AD6 (company_id, source, date)
你的 LIKE-hack 是在欺骗优化器,所以它使用不同的计划(很可能首先使用 IDX_6F7146F0AA9E377A 索引)。 你应该能在解释中看到这个。
我认为你的情况的真正问题是解释的第二行:服务器对 6716 行执行多个函数(MONTH,YEAR),然后尝试对所有这些行进行分组。在此期间,应存储所有这 6716 行(根据您的服务器配置存储在内存或磁盘上)。
SELECT COUNT(*) FROM commission WHERE (date BETWEEN '2018-01-01' AND '2018-12-31') AND company_id = 90 AND source = 'ACTUAL';
=> 我们在谈论多少行?
如果上述查询中的数字远低于 6716,我会尝试在列 customer_id、company_id、来源和日期上添加覆盖索引。不确定最佳顺序,因为它取决于您拥有的数据(检查这些列的基数)。我从索引开始(日期,company_id,来源,customer_id)。另外,我会在合作伙伴上添加唯一索引(id,district_id,owner_id)。
也可以添加额外的生成存储列_year和_month(如果你的服务器有点旧你可以添加普通列并用触发器填充它们)以摆脱多重函数执行。
这是优化器在您的查询中看到的内容。
检查GROUP BY
是否使用索引:
- 函数(
YEAR()
)在GROUP BY
,所以没有。 - 提到了多个 table(
c
和p
),所以没有。
对于 JOIN
,优化器将(几乎总是)从一个开始,然后进入另一个。那么,让我们看看这两个选项:
如果开始 p
:
假设你有 PRIMARY KEY(id)
,没有什么可考虑的。它只会使用该索引。
对于从 p
中选择的每一行,它将查看 c
,并且此 INDEX
的任何变化都是最佳的。
c: INDEX(company_id, source, customer_id, -- in any order (all are tested "=")
date) -- last, since it is tested as a range
如果开始与c
:
c: INDEX(company_id, source, -- in any order (all are tested "=")
date) -- last, since it is tested as a range
-- slightly better:
c: INDEX(company_id, source, -- in any order (all are tested "=")
date, -- last, since it is tested as a range
customer_id) -- really last -- added only to make it "covering".
优化器将查看 "statistics" 以粗略地决定从哪个 table 开始。所以,添加我建议的所有索引。
"covering" 索引包含 所有 查询中任何地方 所需的列。 有时 明智的做法是用更多列扩展 'good' 索引,使其成为 "covering".
但是这里有一个活动扳手。 c.customer_id = p.id
表示 customer_id IN (...)
实际上存在。但是现在有两个 "range-like" 约束——一个是 IN
,另一个是 'range'。在一些较新的版本中,由于 IN
和 仍然 能够进行 "range" 扫描,优化器将愉快地跳来跳去。所以,我推荐这个顺序:
-
column = constant
的测试
- 使用
IN
进行测试
- 一个 'range' 测试(
BETWEEN
、>=
、LIKE
带尾随通配符等) - 也许添加更多的列以使其成为 "covering" -- 但如果您最终在索引中有超过 5 个列,请不要执行此步骤。
因此,对于c
,以下是WHERE
的最佳选择,恰好是"covering"。
INDEX(company_id, source, -- first, but in any order (all "=")
customer_id, -- "IN"
date) -- last, since it is tested as a range
p: (same as above)
因为有 IN
或 "range",所以看索引是否也可以处理 GROUP BY
没有用。
关于 COUNT(x)
的注释 -- 它检查 x
是否为 NOT NULL
。 通常和COUNT(*)
一样正确,它计算行数而不进行任何额外检查。
这是一个非启动器,因为它在函数中隐藏了索引列 (id
):
AND (',3062,3063,3064,3065,3066,...6368,6421,'
LIKE CONCAT('%,', p.id, ',%'))