为最近的事件组优化大型 MySQL 查询(73MM 行)
Optimizing large MySQL query (73MM rows) for most recent event group by
我正在尝试为每个 'lead' 抓取最近的事件。我已经创建了索引,这个查询仍然需要 30 多分钟。
SELECT l.id,
l.home_number,
l.mobile_number,
CASE WHEN l.soldprice < 2 THEN 0 ELSE 1 END as sold,
l.lead_date
FROM (
SELECT l.home_number, MAX(l.id) as id
FROM lead l
WHERE l.lead_date >= DATE_SUB(NOW(), INTERVAL 52 WEEK)
AND l.state NOT IN ('NY','AR','VT','WV','GA','CT','DC','SD')
GROUP BY l.home_number) a
JOIN lead l ON l.id=a.id;
我的 table 索引如下:
Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_typ
lead 0 PRIMARY 1 id A 63123648 NULL NULL BTREE
lead 1 id 1 id A 63266540 NULL NULL BTREE
lead 1 soldprice 1 soldprice A 14715 NULL NULL YES BTREE
lead 1 lead_date 1 lead_date A 15351477 NULL NULL YES BTREE
还有我的 table 架构:
CREATE TABLE lead
(
id BIGINT unsigned NOT NULL,
lead_date DATETIME NULL,
first_name VARCHAR(50) NULL,
last_name VARCHAR(50) NULL,
hashed_ssn VARCHAR(34) NULL,
city VARCHAR(50) NULL,
state VARCHAR(2) NULL,
home_number VARCHAR(10) NULL,
mobile_number VARCHAR(10) NULL,
email VARCHAR(255) NULL,
soldprice DECIMAL(5,2) NULL,
requested_amount INT NULL,
time_zone VARCHAR(5),
camp_id VARCHAR(9),
leadtype_id VARCHAR(3),
hittype_id VARCHAR(3),
PRIMARY KEY (id)
);
如有任何建议,我们将不胜感激。
编辑:我正在使用 MySQL 版本 5.7.19-0ubuntu0.16.04.1
Tl;dr 您需要一个复合(多列)索引。
专业提示:不要创建大量单列索引,除非您知道自己需要它们。它们在复杂的查询中很少有帮助,而且它们会减慢插入和更新的速度。
您在使用子查询筛选要获取的行的 id
值方面做得很好。尽管如此,大部分时间肯定会进入您的子查询,这:
SELECT l.home_number, MAX(l.id) as id
FROM lead l
WHERE l.lead_date >= DATE_SUB(NOW(), INTERVAL 52 WEEK)
AND l.state NOT IN ('NY','AR','VT','WV','GA','CT','DC','SD')
GROUP BY l.home_number
调试子查询然后将它们加入主查询通常是明智的做法。
要做的第一件事是:在 (lead_date, home_number, id)
上创建复合索引。然后 运行 这个简化的子查询,省略了对状态的排除。这应该很快,因为它可以随机访问日期,然后使用索引来处理分组,并使用松散的索引扫描来获取最大 id 值。
SELECT l.home_number, MAX(l.id) as id
FROM lead l
WHERE l.lead_date >= DATE_SUB(NOW(), INTERVAL 52 WEEK)
GROUP BY l.home_number
接下来,尝试在 (lead_date, state, home_number, id)
上创建复合索引并尝试您的原始查询。如果它相当快,你就完成了。您的查询会更快。删除第一个复合索引。
但也可能不是,因为 MySQL 在大量使用 NOT IN
子句时效果不佳。
在那种情况下,保留第一个复合索引并删除第二个复合索引,并将您的状态排除移动到外部查询。
看起来像这样:
SELECT l.id,
l.home_number,
l.mobile_number,
CASE WHEN l.soldprice < 2 THEN 0 ELSE 1 END as sold,
l.lead_date
FROM (
SELECT l.home_number, MAX(l.id) as id
FROM lead l
WHERE l.lead_date >= DATE_SUB(NOW(), INTERVAL 52 WEEK)
GROUP BY l.home_number) a
JOIN lead l ON l.id=a.id
WHERE l.state NOT IN ('NY','AR','VT','WV','GA','CT','DC','SD')
这应该有所帮助。
http://use-the-index-luke.com/对于这类工作来说是一个很好的参考。
由于子查询的条件,这是一个难以优化的查询。
作为一般规则,您可以使用索引来优化某些条件,但只能使用一个范围谓词或 GROUP BY 或 ORDER BY。
但是你有两个范围谓词和一个 GROUP BY:
l.lead_date >= DATE_SUB(NOW(), INTERVAL 52 WEEK)
l.state NOT IN ('NY','AR','VT','WV','GA','CT','DC','SD')
GROUP BY l.home_number
您可以在 lead_date
上使用索引来缩小行选择范围。您可以使用 state
上的索引来缩小行选择范围。或者你可以使用索引来帮助查询按组顺序读取并尽量避免临时table。 但是您只能在给定查询中使用这三种优化中的一种。
接下来的诀窍就是选择您要优先考虑的问题。这取决于每个人在给定您拥有的数据分布的情况下如何改进您的查询。这取决于你的数据,这不是我们可以回答的。因此,您必须使用 EXPLAIN 或仅 运行 带有分析的查询来测试所有三种情况,以查看它有多大帮助。
通常,使用缩小到最小行子集的范围谓词。然后即使其他范围谓词和 GROUP BY 必须在没有索引帮助的情况下工作,它们只需要在较小的行集上工作,所以总成本不是那么糟糕(希望如此)。
我要冒险对数据做出一些假设。
SELECT l.id, l.home_number, l.mobile_number,
(l.soldprice < 2) as sold,
l.lead_date
FROM
(
SELECT l.home_number, MAX(l.id) as maxid
FROM lead l
GROUP BY l.home_number
) a
JOIN lead l ON l.id = a.maxid;
WHERE l.lead_date >= DATE_SUB(NOW(), INTERVAL 52 WEEK)
AND l.state NOT IN ('NY','AR','VT', 'WV','GA','CT','DC', 'SD' )
并且有
INDEX(home_number, id)
假设:
- 多亏了索引,子查询会很快。
- 优化器将在查看
WHERE
之前 运行 子查询。 (如果失败,请将其更改为 HAVING
。)
- MAX(id) 和 "in the last year" 密切相关
- 每个
home_number
都特定于特定的 state
。
让我们知道这是否会得到相同的结果,但速度要快得多。
我正在尝试为每个 'lead' 抓取最近的事件。我已经创建了索引,这个查询仍然需要 30 多分钟。
SELECT l.id,
l.home_number,
l.mobile_number,
CASE WHEN l.soldprice < 2 THEN 0 ELSE 1 END as sold,
l.lead_date
FROM (
SELECT l.home_number, MAX(l.id) as id
FROM lead l
WHERE l.lead_date >= DATE_SUB(NOW(), INTERVAL 52 WEEK)
AND l.state NOT IN ('NY','AR','VT','WV','GA','CT','DC','SD')
GROUP BY l.home_number) a
JOIN lead l ON l.id=a.id;
我的 table 索引如下:
Table Non_unique Key_name Seq_in_index Column_name Collation Cardinality Sub_part Packed Null Index_typ
lead 0 PRIMARY 1 id A 63123648 NULL NULL BTREE
lead 1 id 1 id A 63266540 NULL NULL BTREE
lead 1 soldprice 1 soldprice A 14715 NULL NULL YES BTREE
lead 1 lead_date 1 lead_date A 15351477 NULL NULL YES BTREE
还有我的 table 架构:
CREATE TABLE lead
(
id BIGINT unsigned NOT NULL,
lead_date DATETIME NULL,
first_name VARCHAR(50) NULL,
last_name VARCHAR(50) NULL,
hashed_ssn VARCHAR(34) NULL,
city VARCHAR(50) NULL,
state VARCHAR(2) NULL,
home_number VARCHAR(10) NULL,
mobile_number VARCHAR(10) NULL,
email VARCHAR(255) NULL,
soldprice DECIMAL(5,2) NULL,
requested_amount INT NULL,
time_zone VARCHAR(5),
camp_id VARCHAR(9),
leadtype_id VARCHAR(3),
hittype_id VARCHAR(3),
PRIMARY KEY (id)
);
如有任何建议,我们将不胜感激。
编辑:我正在使用 MySQL 版本 5.7.19-0ubuntu0.16.04.1
Tl;dr 您需要一个复合(多列)索引。
专业提示:不要创建大量单列索引,除非您知道自己需要它们。它们在复杂的查询中很少有帮助,而且它们会减慢插入和更新的速度。
您在使用子查询筛选要获取的行的 id
值方面做得很好。尽管如此,大部分时间肯定会进入您的子查询,这:
SELECT l.home_number, MAX(l.id) as id
FROM lead l
WHERE l.lead_date >= DATE_SUB(NOW(), INTERVAL 52 WEEK)
AND l.state NOT IN ('NY','AR','VT','WV','GA','CT','DC','SD')
GROUP BY l.home_number
调试子查询然后将它们加入主查询通常是明智的做法。
要做的第一件事是:在 (lead_date, home_number, id)
上创建复合索引。然后 运行 这个简化的子查询,省略了对状态的排除。这应该很快,因为它可以随机访问日期,然后使用索引来处理分组,并使用松散的索引扫描来获取最大 id 值。
SELECT l.home_number, MAX(l.id) as id
FROM lead l
WHERE l.lead_date >= DATE_SUB(NOW(), INTERVAL 52 WEEK)
GROUP BY l.home_number
接下来,尝试在 (lead_date, state, home_number, id)
上创建复合索引并尝试您的原始查询。如果它相当快,你就完成了。您的查询会更快。删除第一个复合索引。
但也可能不是,因为 MySQL 在大量使用 NOT IN
子句时效果不佳。
在那种情况下,保留第一个复合索引并删除第二个复合索引,并将您的状态排除移动到外部查询。
看起来像这样:
SELECT l.id,
l.home_number,
l.mobile_number,
CASE WHEN l.soldprice < 2 THEN 0 ELSE 1 END as sold,
l.lead_date
FROM (
SELECT l.home_number, MAX(l.id) as id
FROM lead l
WHERE l.lead_date >= DATE_SUB(NOW(), INTERVAL 52 WEEK)
GROUP BY l.home_number) a
JOIN lead l ON l.id=a.id
WHERE l.state NOT IN ('NY','AR','VT','WV','GA','CT','DC','SD')
这应该有所帮助。
http://use-the-index-luke.com/对于这类工作来说是一个很好的参考。
由于子查询的条件,这是一个难以优化的查询。
作为一般规则,您可以使用索引来优化某些条件,但只能使用一个范围谓词或 GROUP BY 或 ORDER BY。
但是你有两个范围谓词和一个 GROUP BY:
l.lead_date >= DATE_SUB(NOW(), INTERVAL 52 WEEK)
l.state NOT IN ('NY','AR','VT','WV','GA','CT','DC','SD')
GROUP BY l.home_number
您可以在 lead_date
上使用索引来缩小行选择范围。您可以使用 state
上的索引来缩小行选择范围。或者你可以使用索引来帮助查询按组顺序读取并尽量避免临时table。 但是您只能在给定查询中使用这三种优化中的一种。
接下来的诀窍就是选择您要优先考虑的问题。这取决于每个人在给定您拥有的数据分布的情况下如何改进您的查询。这取决于你的数据,这不是我们可以回答的。因此,您必须使用 EXPLAIN 或仅 运行 带有分析的查询来测试所有三种情况,以查看它有多大帮助。
通常,使用缩小到最小行子集的范围谓词。然后即使其他范围谓词和 GROUP BY 必须在没有索引帮助的情况下工作,它们只需要在较小的行集上工作,所以总成本不是那么糟糕(希望如此)。
我要冒险对数据做出一些假设。
SELECT l.id, l.home_number, l.mobile_number,
(l.soldprice < 2) as sold,
l.lead_date
FROM
(
SELECT l.home_number, MAX(l.id) as maxid
FROM lead l
GROUP BY l.home_number
) a
JOIN lead l ON l.id = a.maxid;
WHERE l.lead_date >= DATE_SUB(NOW(), INTERVAL 52 WEEK)
AND l.state NOT IN ('NY','AR','VT', 'WV','GA','CT','DC', 'SD' )
并且有
INDEX(home_number, id)
假设:
- 多亏了索引,子查询会很快。
- 优化器将在查看
WHERE
之前 运行 子查询。 (如果失败,请将其更改为HAVING
。) - MAX(id) 和 "in the last year" 密切相关
- 每个
home_number
都特定于特定的state
。
让我们知道这是否会得到相同的结果,但速度要快得多。