为啥我的 MySQL 组这么慢?

Posted

技术标签:

【中文标题】为啥我的 MySQL 组这么慢?【英文标题】:Why is my MySQL group by so slow?为什么我的 MySQL 组这么慢? 【发布时间】:2012-10-17 17:14:03 【问题描述】:

我正在尝试查询接近 20M 行的分区表(按月)。我需要按 DATE(transaction_utc) 以及 country_id 进行分组。如果我关闭 group by 和聚合返回的行刚刚超过 40k,这并不算多,但是添加 group by 会使查询显着变慢,除非所述 GROUP BY 在 transaction_utc 列上,在这种情况下它变得很快。

我一直在尝试通过调整查询和/或索引来优化下面的第一个查询,并达到了下面的点(大约是最初的 2 倍)但是仍然坚持使用 5s 查询来汇总 45k 行,这似乎太多了。

作为参考,这个盒子是一个全新的 24 个逻辑核心,64GB RAM,Mariadb-5.5.x 服务器,其可用的 INNODB 缓冲池比服务器上的索引空间多得多,因此不应该有任何 RAM 或 CPU 压力。

所以,我正在寻找有关导致速度变慢的原因的想法以及加快速度的建议。任何反馈将不胜感激! :)

好的,进入细节...

以下查询(我真正需要的)大约需要 5 秒 (+/-),并且返回少于 100 行。

SELECT lss.`country_id` AS CountryId
, Date(lss.`transaction_utc`) AS TransactionDate
, c.`name` AS CountryName,  lss.`country_id` AS CountryId
, COALESCE(SUM(lss.`sale_usd`),0) AS SaleUSD
, COALESCE(SUM(lss.`commission_usd`),0) AS CommissionUSD  
FROM `sales` lss  
JOIN `countries` c ON lss.`country_id` = c.`country_id`  
WHERE ( lss.`transaction_utc` BETWEEN '2012-09-26' AND '2012-10-26' AND lss.`username` = 'someuser' )  GROUP BY lss.`country_id`, DATE(lss.`transaction_utc`)

EXPLAIN SELECT 对同一查询如下。请注意,它没有使用 transaction_utc 键。它不应该使用我的覆盖索引吗?

id  select_type table   type    possible_keys   key key_len ref rows    Extra
1   SIMPLE  lss ref idx_unique,transaction_utc,country_id   idx_unique  50  const   1208802 Using where; Using temporary; Using filesort
1   SIMPLE  c   eq_ref  PRIMARY PRIMARY 4   georiot.lss.country_id  1   

现在谈谈我试图确定发生了什么的其他几个选项......

以下查询(更改分组依据)大约需要 5 秒 (+/-),并且只返回 3 行:

SELECT lss.`country_id` AS CountryId
, DATE(lss.`transaction_utc`) AS TransactionDate
, c.`name` AS CountryName,  lss.`country_id` AS CountryId
, COALESCE(SUM(lss.`sale_usd`),0) AS SaleUSD
, COALESCE(SUM(lss.`commission_usd`),0) AS CommissionUSD  
FROM `sales` lss  
JOIN `countries` c ON lss.`country_id` = c.`country_id`  
WHERE ( lss.`transaction_utc` BETWEEN '2012-09-26' AND '2012-10-26' AND lss.`username` = 'someuser' )  GROUP BY lss.`country_id`

以下查询(删除分组依据)需要 4-5 秒 (+/-) 并返回 1 行:

SELECT lss.`country_id` AS CountryId
    , DATE(lss.`transaction_utc`) AS TransactionDate
    , c.`name` AS CountryName,  lss.`country_id` AS CountryId
    , COALESCE(SUM(lss.`sale_usd`),0) AS SaleUSD
    , COALESCE(SUM(lss.`commission_usd`),0) AS CommissionUSD  
    FROM `sales` lss  
    JOIN `countries` c ON lss.`country_id` = c.`country_id`  
    WHERE ( lss.`transaction_utc` BETWEEN '2012-09-26' AND '2012-10-26' AND lss.`username` = 'someuser' )

以下查询需要 0.00X 秒 (+/-) 并返回约 45k 行。这对我来说表明,我们最多只尝试将 45K 行分组到少于 100 个组中(如我的初始查询中所示):

SELECT lss.`country_id` AS CountryId
    , DATE(lss.`transaction_utc`) AS TransactionDate
    , c.`name` AS CountryName,  lss.`country_id` AS CountryId
    , COALESCE(SUM(lss.`sale_usd`),0) AS SaleUSD
    , COALESCE(SUM(lss.`commission_usd`),0) AS CommissionUSD  
    FROM `sales` lss  
    JOIN `countries` c ON lss.`country_id` = c.`country_id`  
    WHERE ( lss.`transaction_utc` BETWEEN '2012-09-26' AND '2012-10-26' AND lss.`username` = 'someuser' )
GROUP BY lss.`transaction_utc`

表格架构:

CREATE TABLE IF NOT EXISTS `sales` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `user_linkshare_account_id` int(11) unsigned NOT NULL,
  `username` varchar(16) NOT NULL,
  `country_id` int(4) unsigned NOT NULL,
  `order` varchar(16) NOT NULL,
  `raw_tracking_code` varchar(255) DEFAULT NULL,
  `transaction_utc` datetime NOT NULL,
  `processed_utc` datetime NOT NULL ,
  `sku` varchar(16) NOT NULL,
  `sale_original` decimal(10,4) NOT NULL,
  `sale_usd` decimal(10,4) NOT NULL,
  `quantity` int(11) NOT NULL,
  `commission_original` decimal(10,4) NOT NULL,
  `commission_usd` decimal(10,4) NOT NULL,
  `original_currency` char(3) NOT NULL,
  PRIMARY KEY (`id`,`transaction_utc`),
  UNIQUE KEY `idx_unique` (`username`,`order`,`processed_utc`,`sku`,`transaction_utc`),
  KEY `raw_tracking_code` (`raw_tracking_code`),
  KEY `idx_usd_amounts` (`sale_usd`,`commission_usd`),
  KEY `idx_countries` (`country_id`),
  KEY `transaction_utc` (`transaction_utc`,`username`,`country_id`,`sale_usd`,`commission_usd`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8
/*!50100 PARTITION BY RANGE ( TO_DAYS(`transaction_utc`))
(PARTITION pOLD VALUES LESS THAN (735112) ENGINE = InnoDB,
 PARTITION p201209 VALUES LESS THAN (735142) ENGINE = InnoDB,
 PARTITION p201210 VALUES LESS THAN (735173) ENGINE = InnoDB,
 PARTITION p201211 VALUES LESS THAN (735203) ENGINE = InnoDB,
 PARTITION p201212 VALUES LESS THAN (735234) ENGINE = InnoDB,
 PARTITION pMAX VALUES LESS THAN MAXVALUE ENGINE = InnoDB) */ AUTO_INCREMENT=19696320 ;

【问题讨论】:

您是否检查过EXPLAIN PARTITIONS <your query> 以确保只扫描有效分区? 【参考方案1】:

有问题的部分可能是GROUP BY DATE(transaction_utc)。您还声称对此查询有一个覆盖索引,但我没有看到。您的 5 列索引包含查询中使用的所有列,但不是按最佳顺序排列(即:WHERE - GROUP BY - SELECT)。

因此,找不到有用索引的引擎将不得不为所有 20M 行评估此函数。实际上,它找到了一个以usernameidx_unique)开头的索引并使用它,因此它必须评估(仅)120 万行的函数。如果您有(transaction_utc)(username, transaction_utc),它将选择三者中最有用的。

您能否通过将列拆分为日期和时间部分来更改表结构? 如果可以,那么在(username, country_id, transaction_date) 或(更改用于分组的两列的顺序)上的索引(username, transaction_date, country_id) 将非常有效。

(username, country_id, transaction_date, sale_usd, commission_usd) 上的覆盖索引更好。


如果您想保留当前结构,请尝试将 5 列索引中的顺序更改为:

(username, country_id, transaction_utc, sale_usd, commission_usd)

或到:

(username, transaction_utc, country_id, sale_usd, commission_usd)

由于您使用的是 MariaDB,因此您可以使用 VIRTUAL columns 功能,而无需更改现有列:

添加一个虚拟(持久)列和适当的索引:

ALTER TABLE sales 
    ADD COLUMN transaction_date DATE NOT NULL
               AS DATE(transaction_utc) 
               PERSISTENT 
    ADD INDEX special_IDX 
        (username, country_id, transaction_date, sale_usd, commission_usd) ;

【讨论】:

是的,我考虑过拆分日期/时间,但认为还有其他问题。我确实以不同的顺序在这些相同的字段上有一个索引,你说我应该只修改索引中字段的顺序? 是的,索引中列的顺序很重要。唯一可能更好(甚至更好)的其他索引是如果您转置两列:(username, transaction_utc, country_id) 不会改变查询的顺序吗? (我看到这在性能上没有明显差异)。 WHERE ( lss.username = 'someuser' AND lss.transaction_utc BETWEEN '2012-09-26' AND '2012-10-26') 或者将查询保持原样并按照您的说明更改索引会更好?我想我不清楚查询每个部分的执行顺序,因为我认为它需要是(“WHERE CLAUSE ITEMS”,“GROUP BY ITEMS”,“JOIN ITEMS”,“RETURNED ITEMS”)。 WHERE 中的顺序无关紧要。 WHERE a_condition AND b_conditionWHERE b_condition AND a_condition 是等价的,优化器知道这一点。 有趣。我认为选择索引时顺序很重要,但我想我需要更详细地阅读文档。 VIRTUAL 是我不知道的东西,但我绝对可以在其他地方利用(也可能在这里)。感谢那!我将尝试您的索引重新排序建议,看看它是如何工作的。

以上是关于为啥我的 MySQL 组这么慢?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的 MongoDB 聚合查询这么慢

为啥我的解决方案这么慢,如何提高查询的性能?

Node - 为啥我的 gif 在使用 GifEncoder 时这么慢

为啥我的haskell程序这么慢? Haskell 编程,人生游戏

为啥我的 pbkdf2 实现这么慢(与 SQLCipher 相比)?

为啥我的分拣程序这么慢? (java中的基数/桶排序)