6

问题 - 以半小时为间隔有效地检索小计总和

我正在使用 MySQL,并且我有一个包含不同时间小计的表。我想从早上 7 点到 12 点每隔半小时检索这些销售额的总和。我当前的解决方案(如下)有效,但查询大约 150,000 条记录需要 13 秒。我打算将来有几百万条记录,而我目前的方法太慢了。

我怎样才能使它更有效,或者如果可能的话,用纯 SQL 替换 PHP 组件?另外,如果我使用 Unix 时间戳而不是日期和时间列,它会帮助您的解决方案更加高效吗?

表名称 - 收据

subtotal    date        time      sale_id
--------------------------------------------
   6        09/10/2011  07:20:33     1
   5        09/10/2011  07:28:22     2
   3        09/10/2011  07:40:00     3
   5        09/10/2011  08:05:00     4
   8        09/10/2011  08:44:00     5
...............
  10        09/10/2011  18:40:00     6
   5        09/10/2011  23:05:00     7

期望的结果

像这样的数组:

  • 半小时 1 ::: (7:00 to 7:30) => 小计之和为 11
  • 半小时 2 ::: (7:30 to 8:00) => 小计之和为 3
  • 半小时 3 ::: (8:00 to 8:30) => 小计之和为 5
  • 半小时 4 ::: (8:30 to 9:00) => 小计之和为 8

当前方法

当前的方式使用从早上 7 点开始并递增 1800 秒的 for 循环,相当于半小时。结果,这对数据库进行了大约 34 次查询。

for($n = strtotime("07:00:00"), $e = strtotime("23:59:59"); $n <= $e; $n += 1800) {  

    $timeA = date("H:i:s", $n);
    $timeB = date("H:i:s", $n+1799);

    $query = $mySQL-> query ("SELECT SUM(subtotal)
                              FROM Receipts WHERE time > '$timeA' 
                              AND time < '$timeB'");

    while ($row = $query-> fetch_object()) {
        $sum[] = $row;
    }
}

电流输出

输出只是一个数组,其中:

  • [0] 表示早上 7 点到早上 7:30
  • [1] 代表上午 7:30 至上午 8:00
  • [33] 代表晚上 11:30 到晚上 11:59:59。

    数组(“0” => 10000,“1” => 20000,.......“33” => 5000);

4

7 回答 7

5

您也可以尝试这个单一查询,它应该返回一个包含 30 分钟分组总数的结果集:

SELECT date, MIN(time) as time, SUM(subtotal) as total
FROM `Receipts`
WHERE `date` = '2012-07-30'
GROUP BY hour(time), floor(minute(time)/30)

要有效地运行此操作,请在日期和时间列上添加复合索引。

你应该得到一个结果集,如:

+---------------------+--------------------+
| time                | total              |
+---------------------+--------------------+
| 2012-07-30 00:00:00 |        0.000000000 |
| 2012-07-30 00:30:00 |        0.000000000 |
| 2012-07-30 01:00:00 |        0.000000000 |
| 2012-07-30 01:30:00 |        0.000000000 |
| 2012-07-30 02:00:00 |        0.000000000 |
| 2012-07-30 02:30:00 |        0.000000000 |
| 2012-07-30 03:00:00 |        0.000000000 |
| 2012-07-30 03:30:00 |        0.000000000 |
| 2012-07-30 04:00:00 |        0.000000000 |
| 2012-07-30 04:30:00 |        0.000000000 |
| 2012-07-30 05:00:00 |        0.000000000 |
| ...
+---------------------+--------------------+
于 2012-08-01T21:54:15.173 回答
4

首先,我将使用单个 DATETIME 列,但使用 DATE 和 TIME 列将起作用。

您可以使用单个查询一次完成所有工作:

select date,
       hour(`time`) hour_num, 
       IF(MINUTE(`time`) < 30, 0, 1) interval_num, 
       min(`time`) interval_begin,
       max(`time`) interval_end,
       sum(subtotal) sum_subtotal
 from receipts
where date='2012-07-31'
group by date, hour_num, interval_num;
于 2012-08-01T21:49:46.547 回答
2

更新:

由于您不关心任何“丢失”的行,因此我还将假设(可能是错误的)您不担心查询可能会返回不是从上午 7 点到上午 12 点的行。此查询将返回您指定的结果集:

SELECT (HOUR(r.time)-7)*2+(MINUTE(r.time) DIV 30) AS i 
     , SUM(r.subtotal) AS sum_subtotal
  FROM Receipts r
 GROUP BY i
 ORDER BY i

time这将返回从引用该列的表达式派生的周期索引 (i) 。为了获得此查询的最佳性能,您可能希望有一个可用的“覆盖”索引,例如:

ON Receipts(`time`,`subtotal`)

如果您要在date列上包含一个相等谓词(它没有出现在您的解决方案中,但确实出现在“选定”答案的解决方案中,那么最好将该列作为前导索引“覆盖”指数。

ON Receipts(`date`,`time`,`subtotal`)

如果您想确保在上午 7 点之前的时段内不返回任何行,那么您可以简单地HAVING i >= 0在查询中添加一个子句。(早上 7 点之前的行将为 i 生成负数。)

SELECT (HOUR(r.time)-7)*2+(MINUTE(r.time) DIV 30) AS i 
     , SUM(r.subtotal) AS sum_subtotal
  FROM Receipts r
 GROUP BY i
HAVING i >= 0
 ORDER BY i

之前:

我假设您想要一个与您当前返回的结果集相似的结果集,但一举一动。此查询将返回您当前正在检索的相同的 33 行,但有一个额外的列标识期间 (0 - 33)。这与我可以获得的当前解决方案最接近:

SELECT t.i
     , IFNULL(SUM(r.subtotal),0) AS sum_subtotal
  FROM (SELECT (d1.i + d2.i + d4.i + d8.i + d16.i + d32.i) AS i
             , ADDTIME('07:00:00',SEC_TO_TIME((d1.i+d2.i+d4.i+d8.i+d16.i+d32.i)*1800)) AS b_time
             , ADDTIME('07:30:00',SEC_TO_TIME((d1.i+d2.i+d4.i+d8.i+d16.i+d32.i)*1800)) AS e_time
          FROM (SELECT 0 i UNION ALL SELECT 1) d1 CROSS
          JOIN (SELECT 0 i UNION ALL SELECT 2) d2 CROSS
          JOIN (SELECT 0 i UNION ALL SELECT 4) d4 CROSS
          JOIN (SELECT 0 i UNION ALL SELECT 8) d8 CROSS
          JOIN (SELECT 0 i UNION ALL SELECT 16) d16 CROSS
          JOIN (SELECT 0 i UNION ALL SELECT 32) d32
        HAVING i <= 33
       ) t
  LEFT
  JOIN Receipts r ON r.time >= t.b_time AND r.time < t.e_time
 GROUP BY t.i
 ORDER BY t.i

一些重要的注意事项:

只要秒数恰好等于“59”或“00”,您当前的解决方案似乎就可能从“收据”中“丢失”行。

看起来您也不关心日期组件,您只是获得所有日期的单个值。(我可能读错了。)如果是这样,DATE 和 TIME 列的分离有助于解决这个问题,因为您可以在查询中引用裸露的 TIME 列。

date在列上添加 WHERE 子句很容易。例如,要获得仅一天的小计汇总,例如在GROUP BY.

WHERE r.date = '2011-09-10'

覆盖索引ON Receipts(time,subtotal)(如果您还没有覆盖索引)可能有助于提高性能。(如果在日期列上包含相等谓词(如上面的 WHERE 子句中,最合适的覆盖索引可能是ON Receipts(date,time,subtotal).

我假设该time列的数据类型为 TIME。(如果不是,则可能需要对查询(在别名为 的内联视图中t)进行小幅调整,以使(派生的)b_time 和 e_time 列的数据类型与timeReceipts 中列的数据类型匹配。

当给定时间段内收据中没有行时,其他答案中的一些建议解决方案不能保证返回 33 行。“缺少行”对您来说可能不是问题,但它是时间序列和时间段数据的常见问题。

我假设您希望保证返回 33 行。当没有找到与时间段匹配的行时,上面的查询返回零的小计。(我注意到,在这种情况下,您当前的解决方案将返回 NULL。我已经将该 SUM 聚合包装在 IFNULL 函数中,以便在 SUM 为 NULL 时返回 0。)

因此,别名为 as 的内联查询t是一个丑陋的混乱,但它工作得很快。它所做的是生成 33 行,具有不同的整数值 0 到 33。同时,它派生一个“开始时间”和一个“结束时间”,用于将每个周期“匹配”到表time上的Receipts列.

我们注意不要将timeReceipts 表中的列包装在任何函数中,而只引用裸列。而且我们要确保没有任何隐式转换正在进行(这就是为什么我们希望 b_time 和 e__time 的数据类型匹配。and函数都返回数据类型。(我们无法绕过匹配和ADDTIMEGROUP通过操作。)SEC_TO_TIMETIME

最后一个时段的“结束时间”值返回为“24:00:00”,我们通过运行以下测试验证这是匹配的有效时间:

SELECT MAKETIME(23,59,59) < MAKETIME(24,0,0)

这是成功的(返回 1)所以我们很好。

派生列 (t.b_timet.e_time) 也可以包含在结果集中,但创建数组时不需要它们,如果不包含它们,它(可能)效率更高。


最后一点:为了获得最佳性能,将别名为实际表的内联视图加载t到实际表中可能是有益的(临时表就可以了。),然后您可以引用该表来代替内联视图。这样做的好处是您可以在该表上创建索引。

于 2012-08-01T23:16:29.097 回答
0

在我的查询中,我假设一个名为 date 的日期时间字段。这将为您提供从您指定的任何日期时间开始的所有组:

SELECT 
  ABS(FLOOR(TIMESTAMPDIFF(MINUTE, date, '2011-08-01 00:00:00') / 30)) AS GROUPING
  , SUM(subtotal) AS subtotals 
FROM 
  Receipts 
GROUP BY 
  ABS(FLOOR(TIMESTAMPDIFF(MINUTE, date, '2011-08-01 00:00:00') / 30))
ORDER BY
  GROUPING
于 2012-08-01T22:06:17.600 回答
0

始终为您的数据使用正确的数据类型。对于您的日期/时间列,最好将它们存储为(最好是 UTC 分区)时间戳。尤其如此,因为某些日期不存在某些时间(对于某些时区,因此是 UTC)。您将需要此列的索引。

此外,您的日期/时间范围不会给您想要的 - 即,您会在小时内准确地错过任何东西(因为您使用严格的大于比较)。始终将范围定义为“包含下限,上限不包含”(so, time >= '07:00:00' AND time < '07:30:00')。这对于时间戳尤其重要,因为它需要处理更多的字段。

因为 mySQL 没有递归查询,所以您需要几个额外的表来实现这一点。我将它们称为“永久”表,但如果有必要,当然可以在线定义它们。

您将需要一个日历表。出于多种原因,这些很有用,但在这里我们希望它们用于列出日期。如有必要,这将允许我们显示小计为 0 的日期。出于同样的原因,您还需要一个以半小时为增量的时间值。

这应该允许您像这样查询您的数据:

SELECT division, COALESCE(SUM(subtotal), 0)
FROM (SELECT TIMESTAMP(calendar_date, clock_time) as division
      FROM Calendar
      CROSS JOIN Clock
      WHERE calendar_date >= DATE('2011-09-10') 
      AND calendar_date < DATE('2011-09-11')) as divisions
LEFT JOIN Sales_Data
ON occurredAt >= division 
AND occurredAt < division + INTERVAL 30 MINUTE
GROUP BY division

SQLFiddle 上的工作示例,为了简洁起见,它使用常规JOIN

于 2012-08-01T22:32:53.193 回答
0

使其成为纯 SQL 的一种方法是使用查找表。我不太了解MySql,所以代码可能会有很多改进。我所有的代码都将是 Ms Sql.. 我会这样做:

   /* Mock salesTable */
   Declare @SalesTable TABLE (SubTotal int, SaleDate datetime)
Insert into @SalesTable (SubTotal, SaleDate) VALUES (1, '2012-08-01 12:00')
Insert into @SalesTable (SubTotal, SaleDate) VALUES (2, '2012-08-01 12:10')
Insert into @SalesTable (SubTotal, SaleDate) VALUES (3, '2012-08-01 12:15')
Insert into @SalesTable (SubTotal, SaleDate) VALUES (4, '2012-08-01 12:30')
Insert into @SalesTable (SubTotal, SaleDate) VALUES (5, '2012-08-01 12:35')
Insert into @SalesTable (SubTotal, SaleDate) VALUES (6, '2012-08-01 13:00')
Insert into @SalesTable (SubTotal, SaleDate) VALUES (7, '2012-08-01 14:00')

/* input data */
declare @From datetime, @To DateTime, @intervall int 
set @from = '2012-08-01' 
set @to = '2012-08-02'
set @intervall = 30

/* Create lookup table */
DECLARE @lookup TABLE (StartTime datetime, EndTime datetime) 
DECLARE @tmpTime datetime
SET @tmpTime = @from
WHILE (@tmpTime <= @To) 
BEGIN
 INSERT INTO @lookup (StartTime, EndTime) VALUES (@tmpTime, dateAdd(mi, @intervall, @tmpTime))
 set @tmpTime = dateAdd(mi, @intervall, @tmpTime)
END

/* Get data */
select l.StartTime, l.EndTime, sum(subTotal) from @SalesTable as SalesTable 
    join @lookUp as l on SalesTable.SaleDate >= l.StartTime and SalesTable.SaleDate < l.EndTime
    group by l.StartTime, l.EndTime
于 2012-08-01T21:56:09.523 回答
0

我也找到了一个不同的解决方案,并在此处发布以供任何人偶然发现。以半小时为间隔分组。

SELECT SUM(total), time, date
FROM tableName
GROUP BY (2*HOUR(time) + FLOOR(MINUTE(time)/30))

链接了解更多信息 http://www.artfulsoftware.com/infotree/queries.php#106

于 2012-08-02T16:39:24.427 回答