问题 - 有效地半小时间隔检索小计的总和
我正在使用MySQL,我有一个包含不同时间小计的表。我希望从早上7点到早上12点以半小时为间隔检索这些销售额的总和。我当前的解决方案(下面)工作,但需要13秒来查询大约150,000条记录。我打算将来有几百万条记录,而我目前的方法太慢了。
我如何才能提高效率,或者如果可能的话用纯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
期望的结果
这样的数组:
当前方法
当前的方式使用for循环,从早上7点开始,增加1800秒,相当于半小时。因此,这会对数据库进行大约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;
}
}
当前输出
输出只是一个数组:
[33]代表晚上11:30至晚上11:59:59。
数组(“0”=&gt; 10000, “1”=&gt; 20000, .............. “33”=&gt; 5000);
答案 0 :(得分: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 |
| ...
+---------------------+--------------------+
答案 1 :(得分: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;
答案 2 :(得分: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 AM之前没有返回任何行,那么您只需在查询中添加HAVING i >= 0
子句即可。 (7 AM之前的行数会为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
PREVIOUSLY:
我假设您想要一个与您当前返回的结果类似的结果集,但是一下子。此查询将返回您当前正在检索的相同的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子句。
WHERE r.date = '2011-09-10'
覆盖索引ON Receipts(time,subtotal)
(如果您还没有覆盖索引)可能有助于提高性能。 (如果在日期列中包含等式谓词(如上面的WHERE子句中那样,最合适的覆盖索引可能是ON Receipts(date,time,subtotal)
。
我假设time
列的数据类型为TIME。 (如果不是,则可能需要对查询进行小调整(在内联视图中别名为t
),以使(派生的)b_time和e_time列的数据类型与数据类型匹配。收据中的time
列。
当给定时间段内的收据中没有行时,其他答案中的某些建议解决方案无法保证返回33行。 “缺少行”对您来说可能不是问题,但它是时间序列和时间段数据的常见问题。
我假设您希望保证返回33行。当没有找到与时间段匹配的行时,上面的查询返回小计零。 (我注意到你当前的解决方案在这种情况下会返回一个NULL。我已经把IFN聚合在一个IFNULL函数中,所以当SUM为NULL时它将返回0。)
因此,内联查询别名为t
是一个丑陋的混乱,但它的工作速度很快。它正在做的是生成33行,具有不同的整数值0到33.同时,它导出一个“开始时间”和一个“结束时间”,用于将每个句点“匹配”到{{1} } time
表上的列。
我们注意不要在任何函数中包装Receipts表中的Receipts
列,而只引用bare列。我们希望确保我们没有任何隐式转换(这就是为什么我们希望b_time和e__time的数据类型匹配。time
和ADDTIME
函数都返回SEC_TO_TIME
数据类型。(我们无法绕过匹配和GROUP BY操作。)
上一个句点的“结束时间”值返回为“24:00:00”,我们通过运行此测试验证这是匹配的有效时间:
TIME
成功(返回1)所以我们在那里很好。
派生列(SELECT MAKETIME(23,59,59) < MAKETIME(24,0,0)
和t.b_time
)也可以包含在结果集中,但创建数组不需要它们,如果你不这样做,它可能会更高效包括它们。
最后一点注意:为了获得最佳性能,将内嵌视图别名为t.e_time
加载到实际表中可能是有益的(临时表没问题。),然后您可以引用该表内联视图的位置。这样做的好处是您可以在该表上创建索引。
答案 3 :(得分: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
答案 4 :(得分: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
答案 5 :(得分:0)
始终为数据使用正确的数据类型。对于日期/时间列,最好将它们存储为(最好是UTC分区)时间戳。尤其如此,因为某些日期(某些时间段,因此为UTC)不存在某些时间。您将需要此列的索引。
此外,你的日期/时间范围不会给你你想要的东西 - 也就是说,你错过了一小时内的任何东西(因为你使用了严格的大于比较)。始终将范围定义为“下限包含,上限独占”(因此,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
(Working example on SQLFiddle,为了简洁而使用常规JOIN
答案 6 :(得分: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