MySQL 日历 table 慢的子查询计数

MySQL subquery count with calendar table slow

我在 MySQL (InnoDB) 中有销售额 table。它有 +- 100 万条记录。我想展示一些漂亮的图表。获取正确的数据不是问题。快速获取它是...

所以我喜欢计算 table A 组每天(稍后还有月、年)从 A 期到 Z 期的销售额。具体;在过去的 30 天里,我想知道我们每天在数据库中有多少销售记录。

所以 MySQL 必须 return 像这样:

我喜欢实现 MySQL return 数据如下:

date, count
2017-04-01, 2482
2017-04-02, 1934
2017-04-03, 2701
...

Sales的结构基本是这样的:

CREATE TABLE `sales` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `deleted_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `contacts_created_at_index` (`created_at`),
  KEY `contacts_deleted_at_index` (`deleted_at`),
  KEY `ind_created_at_deleted_at` (`created_at`,`deleted_at`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

有些天(数据点)可能没有任何结果,但我不喜欢数据中存在间隙。所以我也有一些 'calendar' table.

CREATE TABLE `time_dimension` (
  `id` int(11) NOT NULL,
  `db_date` date NOT NULL,
  `year` int(11) NOT NULL,
  `month` int(11) NOT NULL,
  `day` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `td_ymd_idx` (`year`,`month`,`day`),
  UNIQUE KEY `td_dbdate_idx` (`db_date`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

每天用计数获取 30 行(30 天)需要 30 秒...

这是我尝试的第一个查询:

SELECT 
    `db_date` AS `date`,
    (SELECT 
            COUNT(1)
        FROM
            sales
        WHERE
            DATE(created_at) = db_date) AS count
FROM
    `time_dimension`
WHERE
    `db_date` >= '2017-04-11'
        AND `db_date` <= '2017-04-25'
ORDER BY `db_date` ASC

但是就像我说的那样它真的很慢(11.9 秒)。我尝试了各种其他方法,但没有运气。例如:

SELECT time_dimension.db_date AS DATE,
       COUNT(1) AS count
FROM sales RIGHT JOIN time_dimension ON (DATE(sales.created_at) =         
    time_dimension.db_date)
WHERE
    (time_dimension.db_date BETWEEN '2017-03-11' AND '2017-04-11')
GROUP BY
    DATE

仅查询 1 个数据点仅需 5.4 毫秒:

SELECT COUNT(1) FROM sales WHERE created_at BETWEEN '2017-04-11 00:00:00' AND '2017-04-25 23:59:59'

我还没有在我的本地机器上检查 innodb_buffer_poolsize。我也会检查一下。关于如何快速进行查询的任何想法?将来我什至需要 where 子句和连接来过滤销售记录集..

谢谢。

尼克

您可以尝试先统计销售数据,然后将统计结果与您的 日历 table.

SELECT time_dimension.db_date AS date, 
       by_date.sale_count 
FROM   time_dimension 
       LEFT JOIN (SELECT DATE(sales.created_at) sale_date, 
                         COUNT(1)               AS sale_count 
                  FROM   sales 
                  WHERE  created_at BETWEEN '2017-03-11 00:00:00' AND 
                                            '2017-04-11  23:59:59' 
                  GROUP  BY DATE(sales.created_at)) by_date 
              ON time_dimension.db_date = by_date.sale_date 
WHERE  time_dimension.db_date BETWEEN '2017-03-11' AND '2017-04-11' 

您的查询中有问题的部分是数据类型转换 DATE(created_at),这有效地阻止了 Mysql 使用位于 created_at 的索引。

您的 1 datapoint 查询避免了这种情况,这就是它工作速度很快的原因。

要解决此问题,您应该检查 created_at 是否在特定日期的范围内,例如:

created_at BETWEEN db_date AND DATE_ADD(db_date,INTERVAL 1 DAY)

这样 Mysql 就可以根据需要使用索引(进行范围查找)。

 WHERE DATE(created_at) = db_date)

-->

 WHERE created_at >= db_date
   AND created_at  < db_date + INTERVAL 1 DAY
  • 这避免了包括第二天的午夜(正如 BETWEEN 那样)
  • 适用于所有口味:DATEDATETIMEDATETIME(6)
  • 不将 created_at 隐藏在索引看不到的函数中。

对于time_dimension,去掉PRIMARY KEY (id),把UNIQUE(db_date)改成PK

进行这些更改后,您的原始子查询可能会与 LEFT JOIN ( SELECT ... ) 竞争。 (这取决于 MySQL 的版本。)