如何优化依赖于COUNT和GROUP BY的查询?

时间:2017-02-09 21:07:06

标签: mysql sql performance

我有一个查询,其目的是生成有关在不同时段(按月,按季度,按年等)从网站下载了多少音乐作品(曲目)的统计数据。该查询对表entityusageentityusage_filetrack进行操作。

要获取属于特定相册的曲目的下载次数,我会执行以下查询:

select 
    date_format(eu.updated, '%Y-%m-%d') as p, count(eu.id) as c
from        entityusage as eu
inner join  entityusage_file as euf 
        ON  euf.entityusage_id = eu.id
inner join  track as t 
        ON t.id = euf.track_id
where
    t.album_id = '0054a47e-b594-407b-86df-3be078b4e7b7'
        and entitytype = 't'
        and action = 1
group by date_format(eu.updated, '%Y%m%d')

我需要设置entitytype = 't',因为entityusage也可以保存其他实体的下载(如果entitytype = 'a'则会下载整个相册,然后entityusage_file会保留所有曲目这张专辑"翻译成了#34;到了下载点。)

此查询需要40 - 50秒。我一直试图优化这个查询一段时间,但我觉得我接近这个错误的方式。

这是必须运行以生成报告的4个类似查询中的一个。报告最好能够在用户等待时完成。现在,我看了3-4分钟。那是一段漫长的等待时间。

可以使用索引进一步优化此查询,还是需要采用其他方法来完成此工作?

CREATE TABLE `entityusage` (
  `id` char(36) NOT NULL,
  `title` varchar(255) DEFAULT NULL,
  `entitytype` varchar(5) NOT NULL,
  `entityid` char(36) NOT NULL,
  `externaluser` int(10) NOT NULL,
  `action` tinyint(1) NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `e` (`entityid`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

CREATE TABLE `entityusage_file` (
  `id` char(36) NOT NULL,
  `entityusage_id` char(36) NOT NULL,
  `track_id` char(36) NOT NULL,
  `file_id` char(36) NOT NULL,
  `type` varchar(3) NOT NULL,
  `quality` int(1) NOT NULL,
  `size` int(20) NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `file_id` (`file_id`),
  KEY `entityusage_id` (`entityusage_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `track` (
  `id` char(36) NOT NULL,
  `album_id` char(36) NOT NULL,
  `number` int(3) NOT NULL DEFAULT '0',
  `title` varchar(255) DEFAULT NULL,
  `updated` datetime NOT NULL DEFAULT '2000-01-01 00:00:00',
  PRIMARY KEY (`id`),
  KEY `album` (`album_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 CHECKSUM=1 DELAY_KEY_WRITE=1 ROW_FORMAT=DYNAMIC;

查询中的EXPLAIN为我提供了以下内容:

+------+-------------+-------+--------+----------------+----------------+---------+------------------------------+---------+----------------------------------------------+
| id   | select_type | table | type   | possible_keys  | key            | key_len | ref                          | rows    | Extra                                        |
+------+-------------+-------+--------+----------------+----------------+---------+------------------------------+---------+----------------------------------------------+
|    1 | SIMPLE      | eu    | ALL    | NULL           | NULL           | NULL    | NULL                         | 7832817 | Using where; Using temporary; Using filesort |
|    1 | SIMPLE      | euf   | ref    | entityusage_id | entityusage_id | 108     | func                         |       1 | Using index condition                        |
|    1 | SIMPLE      | t     | eq_ref | PRIMARY,album  | PRIMARY        | 108     | trackerdatabase.euf.track_id |       1 | Using where                                  |
+------+-------------+-------+--------+----------------+----------------+---------+------------------------------+---------+----------------------------------------------+

4 个答案:

答案 0 :(得分:2)

这是您的查询:

select date_format(eu.updated, '%Y-%m-%d') as p, count(eu.id) as c
from entityusage eu join
     entityusage_file euf
     on euf.entityusage_id = eu.id join
     track t 
     on t.id = euf.track_id
where t.album_id = '0054a47e-b594-407b-86df-3be078b4e7b7' and
      eu.entitytype = 't' and
      eu.action = 1
group by date_format(eu.updated, '%Y%m%d');

我建议在track(album_id, id)entityusage_file(track_id, entityusage_id)entityusage(id, entitytype, action)上建立索引。

答案 1 :(得分:2)

假设entityusage_file主要是多个:多个映射表,请参阅this以获取有关改进它的提示。请注意,它要求删除id并创建一对2列索引,其中一个是PRIMARY KEY(track_id, entityusage_id)。由于您的表格中有一些额外的列,因此该链接并不涵盖所有内容。

UUID可以从108字节缩减到36,然后通过转到BINARY(16)并使用压缩函数缩小到16。存在许多(包括版本8.0中的内置对); here's我的。{/ p>

解释一件事......查询执行应该以{{1​​}}开始(假设track非常有选择性)。挂断的是没有索引从那里到下一个表。戈登的建议索引包括这样的内容。

'0054a47e-b594-407b-86df-3be078b4e7b7'date_format(eu.updated, '%Y-%m-%d')可以简化为date_format(eu.updated, '%Y%m%d')。 (没有显着的性能变化。)

(其他答案和评论涵盖了一些问题;我在此不再重复。)

答案 2 :(得分:1)

因为GROUP BY操作是在涉及函数的表达式上,所以MySQL不能使用索引来优化该操作。它需要“使用filesort”操作。

鉴于目前的表格定义,我认为Gordon建议的指数是最好的赌注。但即使使用这些索引,“高帖”也是eu表,对所有这些行进行分块和排序。

要获得更合理的性能,您可能需要引入“预先计算结果”表。生成所有东西的计数会很昂贵......但我们可以提前支付这个价格......

CREATE TABLE usage_track_by_day
( updated_dt DATE NOT NULL
, PRIMARY KEY (track_id, updated_dt)
)
AS
SELECT eu.track_id
     , DATE(eu.updated) AS updated_dt
     , SUM(IF(eu.action = 1,1,0) AS cnt
  FROM entityusage eu
 WHERE eu.track_id IS NOT NULL
   AND eu.updated IS NOT NULL
 GROUP
    BY eu.track_id
     , DATE(eu.updated)

索引ON entityusage (track_id,updated,action)可能会使效果受益。

然后,我们可以针对新的“预先计算结果”表编写一个查询,并以合理的性能更好地拍摄。

“预先计算的结果”表格会过时,需要定期刷新。

这不一定是该问题的最佳解决方案,但它是我们可以在数据仓库/数据智能应用程序中使用的技术。这让我们可以通过大量细节行来计算一次,然后保存这些计数以便快速访问。

答案 3 :(得分:1)

你可以尝试这个吗?如果没有你的一些样本数据,我真的无法测试它。 在这种情况下,查询首先在表跟踪中查找并加入其他表。

 SELECT 
    date_format(eu.updated, '%Y-%m-%d') AS p
    , count(eu.id) AS c
FROM track AS t
INNER JOIN entityusage_file AS euf ON t.id = euf.track_id
INNER JOIN entityusage AS eu ON euf.entityusage_id = eu.id
 WHERE
    t.album_id = '0054a47e-b594-407b-86df-3be078b4e7b7'
        AND entitytype = 't'
        AND ACTION = 1
GROUP BY date_format(eu.updated, '%Y%m%d');