SELECT COUNT 对具有 > 100M 行的表进行优化

Posted

技术标签:

【中文标题】SELECT COUNT 对具有 > 100M 行的表进行优化【英文标题】:SELECT COUNT with JOIN optimization for tables with > 100M rows 【发布时间】:2019-02-05 08:29:55 【问题描述】:

我有以下查询

SELECT SUBSTRING(a0_.created_date FROM 1 FOR 10) AS sclr_0, 
       COUNT(1) AS sclr_1 
FROM applications a0_ INNER JOIN 
     package_codes p1_ ON a0_.id = p1_.application_id 
WHERE a0_.created_date BETWEEN '2019-01-01' AND '2020-01-01' AND
      p1_.type = 'Package 1'
GROUP BY sclr_0

--- 编辑 ---

你们中的大多数人都关注 GROUP BY 和 SUBSTRING,但这不是问题的根源。

以下查询具有相同的执行时间:

SELECT COUNT(1) AS sclr_1 
FROM applications a0_ INNER JOIN 
     package_codes p1_ ON a0_.id = p1_.application_id 
WHERE a0_.created_date BETWEEN '2019-01-01' AND '2020-01-01' AND
      p1_.type = 'Package 1'

--- 编辑 2 ---

在 applications.created_date 添加索引并强制查询使用指定索引后,@DDS 建议执行时间降至 ~750ms

当前查询如下:

SELECT SUBSTRING(a0_.created_date FROM 1 FOR 10) AS sclr_0, 
       COUNT(1) AS sclr_1 
FROM applications a0_ USE INDEX (applications_created_date_idx) INNER JOIN 
     package_codes p1_ USE INDEX (PRIMARY, UNIQ_70A9C6AA3E030ACD, package_codes_type_idx) ON a0_.id = p1_.application_id 
WHERE a0_.created_date BETWEEN '2019-01-01' AND '2020-01-01' AND
      p1_.type = 'Package 1'
GROUP BY sclr_0

--- 编辑 3 ---

我发现在查询中使用过多的索引可能会导致在某些情况下 mysql 会使用非最佳索引,因此最终查询应如下所示:

SELECT SUBSTRING(a0_.created_date FROM 1 FOR 10) AS sclr_0, 
       COUNT(1) AS sclr_1 
FROM applications a0_ USE INDEX (applications_created_date_idx) INNER JOIN 
     package_codes p1_ USE INDEX (package_codes_application_idx) ON a0_.id = p1_.application_id 
WHERE a0_.created_date BETWEEN '2019-01-01' AND '2020-01-01' AND
      p1_.type = 'Package 1'
GROUP BY sclr_0

--- 结束编辑 ---

package_codes 包含超过 100.000.000 条记录。

应用程序包含超过 250.000 条记录。

查询需要 2 分钟才能得到结果。有什么办法可以优化吗? 我被困在 MySQL 5.5 上。

表格:

CREATE TABLE `applications` (
  `id` int(11) NOT NULL,
  `created_date` datetime NOT NULL,
  `name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
  `surname` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

ALTER TABLE `applications`
  ADD PRIMARY KEY (`id`),
  ADD KEY `applications_created_date_idx` (`created_date`);

ALTER TABLE `applications`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
CREATE TABLE `package_codes` (
  `id` int(11) NOT NULL,
  `application_id` int(11) DEFAULT NULL,
  `created_date` datetime NOT NULL,
  `type` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
  `code` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
  `disabled` tinyint(1) NOT NULL DEFAULT '0',
  `meta_data` longtext COLLATE utf8mb4_unicode_ci
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

ALTER TABLE `package_codes`
  ADD PRIMARY KEY (`id`),
  ADD UNIQUE KEY `UNIQ_70A9C6AA3E030ACD` (`application_id`),
  ADD KEY `package_codes_code_idx` (`code`),
  ADD KEY `package_codes_type_idx` (`type`),
  ADD KEY `package_codes_application_idx` (`application_id`),
  ADD KEY `package_codes_code_application_idx` (`code`,`application_id`);

ALTER TABLE `package_codes`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;

ALTER TABLE `package_codes`
  ADD CONSTRAINT `FK_70A9C6AA3E030ACD` FOREIGN KEY (`application_id`) REFERENCES `applications` (`id`);

【问题讨论】:

为什么你认为2分钟很长?你的硬件是什么?也许您在磁盘子系统的限制下运行?您是否考虑过定期计算汇总? @AlexYu 此查询用于生成实时的整体统计信息,我们的客户在生产中接受的时间不应超过几秒钟。这台机器在 IMO 上已经足够强大了(4x E7-4860 - 40 核、80 线程、256GB RAM、SSD 上的硬件 RAID 1 并启用了控制器缓存) 尝试改进您的索引。但是,如果您仍然面临此类问题,您可能会选择列存储(至少对于这种“实时”查询)。另外我建议您添加一个带有“substr”结果的新“列”,这样您就不必一直计算它。 【参考方案1】:

我的建议是避免这种情况:

SELECT SUBSTRING(a0_.created_date FROM 1 FOR 10) AS sclr_0, 
[...]  
GROUP BY sclr_0

因为每次 dbms '重新计算'该字段并且不能在其上使用索引时,如果您将此数据放在它自己的列中并在其上创建索引,您的性能应该会提高

或者,至少,使用 date_part 函数,这样 mysql 可以设法使用它的索引(显然你应该在 application.created_date 上添加一个索引)

SELECT SUBSTRING(a0_.created_date FROM 1 FOR 10) AS sclr_0, COUNT(1) AS sclr_1 
FROM applications a0_ INNER JOIN 
     package_codes p1_ ON (a0_.id = p1_.application_id and a0_.created_date 
BETWEEN '2019-01-01' AND '2020-01-01' and p1_.type = 'Package 1')      
FORCE INDEX (date_index, type_index)
Group by date(a0_.created_date)

另一个优化是将条件“推送”到“on”子句,以便 mysql 在加入之前“过滤”数据 -> 在更少的行中执行加入

编辑: 这是在日期上创建索引

CREATE INDEX date_index ON application(created_date);

如果您的类型比日期多得多,您应该考虑将索引放在类型上。

CREATE INDEX type_index ON package_codes(type);

[编辑 2] 请张贴结果

select count(distinct date(a0_.created_date)) as N_DATES, count(distinct type)as N_TYPES
FROM applications a0_ INNER JOIN 
     package_codes p1_ ON a0_.id = p1_.application_id 

只是对女巫指数有一个想法会更有选择性

有用的link 用于使用 MySQL 进行索引优化

【讨论】:

将查询更改为:SELECT a0_.created_date AS sclr_0, COUNT(1) AS sclr_1 FROM applications a0_ INNER JOIN package_codes p1_ ON a0_.id = p1_.application_id WHERE a0_.created_date BETWEEN '2019- 01-01' AND '2020-01-01' AND p1_.type = 'Package 1' GROUP BY year(a0_.created_date), month(a0_.created_date), day(a0_.created_date) 执行时间还是2分钟 您有关于 application.created_date 的索引吗?也尝试在join内推过滤条件(帖子已编辑) 请检查更新的问题描述。问题出在其他地方。简化查询具有相同的执行时间。 添加语法来添加你需要的索引 在 application.created_date 添加索引后执行时间没有变化。我已经更新了数据库创建查询。【参考方案2】:

在 applications.created_date 添加索引并强制查询使用指定索引后,@DDS 建议执行时间降至 ~750ms

最终查询应如下所示:

SELECT SUBSTRING(a0_.created_date FROM 1 FOR 10) AS sclr_0, 
       COUNT(1) AS sclr_1 
FROM applications a0_ USE INDEX (applications_created_date_idx) INNER JOIN 
     package_codes p1_ USE INDEX (package_codes_application_idx) ON a0_.id = p1_.application_id 
WHERE a0_.created_date BETWEEN '2019-01-01' AND '2020-01-01' AND
      p1_.type = 'Package 1'
GROUP BY sclr_0

【讨论】:

这是否意味着您找到了解决方案?完全可以:a)回答您自己的问题,b)选择您自己的答案作为解决方案【参考方案3】:

您需要创建一个复合索引。看来您已经在表上创建了单独的索引。在这种情况下,您需要 package_codes 中 created_date 的单独索引以及 created_date 和 type 的复合索引。

可能会先投射日期,然后再分组。

【讨论】:

“created_date 和类型的复合索引。” - created_date 来自表应用程序,类型来自表 package_codes。 MySQL 允许在两个表之间创建复合索引吗? 抱歉,我正在查看中间查询(在帖子当前编辑的表单中)并没有看到应用程序表。似乎有一些编辑。不,MySQL 不允许跨两个表的复合索引 - 不确定是否有任何 RDBMS。 @cherrysoft - 曾经有一个第 3 方引擎可以跨表创建复合索引。在幕后,它实际上会结合表格来实现这一点。 (已经没有了。) 这很有趣,Rick,组合表格实际上是实现这一目标的一种创新方法。【参考方案4】:

最佳索引是

p1_:  (type, application_id)
a0_:  (created_date, id)

这些适用于所有(?)版本的查询,除了那些“强制”索引。

优化器将尝试决定是从p1_ 还是a0_ 开始。而且,有了这些索引,它应该可以很好地选择更好的表。

SUBSTRING(a0_.created_date FROM 1 FOR 10) 可以简化为DATE(a0_.created_date),但我怀疑它是否会改变性能。

请注意,索引将“覆盖”,从而提供额外的提升。 EXPLAIN 通过说Using index(不是Using index condition)来表示。

进一步改进:去掉package_codes.id,将application_id提升为PRIMARY KEY。这可能会简化查询!

我的建议适用于(也许)所有版本的 MySQL。

【讨论】:

以上是关于SELECT COUNT 对具有 > 100M 行的表进行优化的主要内容,如果未能解决你的问题,请参考以下文章

具有MIN COUNT的MYSQL DB SELECT [重复]

MongoDB 对地理空间查询的计数不准确

如何对具有相同 <select> 选项的 <input> 求和?

count(*) 来自 2 个具有相同列的表

select count()和select count的区别

[真伪]数据库中 select count(1) 比 select count(*) 快?