【问题标题】:Optimizing large MySQL query (73MM rows) for most recent event group by为最近的事件组优化大型 MySQL 查询(73MM 行)
【发布时间】:2017-08-17 18:49:55
【问题描述】:

我正在尝试获取每个“潜在客户”的最新事件。我已经创建了索引,这个查询仍然需要 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 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

还有我的表架构:

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

【问题讨论】:

  • home_number和state之间有关系吗?例如,“212-...”始终是 NY 电话号码;我也许可以利用这一点。

标签: mysql indexing query-optimization query-performance


【解决方案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/ 是此类工作的一个很好的参考。

【讨论】:

  • 谢谢@O。琼斯。我将着手进行设置并报告两者的结果,以防将来有人遇到这种情况。
  • “因为 MySQL 在大量的 NOT IN 子句中表现不佳。”和 NULL(如果您的数据允许)既不是 not in 也不是 in l.state... 在这里可能并不重要,但该列确实允许空值...
【解决方案2】:

由于子查询的条件,这是一个难以优化的查询。

作为一般规则,您可以使用索引来优化某些条件,但只能使用一个范围谓词或 GROUP BY 或 ORDER BY。

但是你有两个范围谓词和一个 GROUP BY:

  • l.lead_date &gt;= 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 上的索引来缩小行选择范围。或者您可以使用索引来帮助查询按组顺序读取,并尽量避免使用临时表。 但您只能在给定查询中使用这三种优化中的一种。

然后诀窍就变成了选择您将优先考虑的那个。考虑到您拥有的数据分布,这取决于每个人如何改进您的查询。这取决于您的数据,这不是我们可以回答的问题。因此,您必须使用 EXPLAIN 测试所有三种情况,或者只使用分析运行查询,看看它有多大帮助。

通常,使用缩小到最小行子集的范围谓词。然后,即使其他范围谓词和 GROUP BY 必须在没有索引的帮助下工作,它们也只需要在较小的一组行上工作,因此总成本还不错(希望如此)。

【讨论】:

  • 谢谢@Bill Karwin。我正在运行上一个建议中的两个测试,并且还将添加您的尝试,即仅在子查询中选择一个谓词并将另外两个移动到外部查询的建议。我将在今天晚些时候报告调查结果。
  • 我不建议将谓词移动到外部查询。您仍然可以在子查询中使用它们,但它们不会得到索引的帮助。
【解决方案3】:

我正在冒险对数据做出一些假设。

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) 与“去年”密切相关
  • 每个home_number 都特定于特定的state。

让我们知道这是否得到相同的结果,但速度更快。

【讨论】:

    猜你喜欢
    • 2014-01-11
    • 1970-01-01
    • 2014-08-06
    • 1970-01-01
    • 2011-06-12
    • 1970-01-01
    • 1970-01-01
    • 2016-04-14
    • 1970-01-01
    相关资源
    最近更新 更多