如何高效地使用 SQL 检索半小时间隔的数据?
Posted
技术标签:
【中文标题】如何高效地使用 SQL 检索半小时间隔的数据?【英文标题】:How to Efficiently use SQL to Retrieve Data on Half Hour Intervals? 【发布时间】:2012-08-01 21:35:38 【问题描述】:问题 - 每隔半小时有效地检索小计总和
我正在使用 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 到 8:00) => 小计之和为 3 半小时 3 ::: (8:00 到 8:30) => 小计之和为 5 半小时 4 ::: (8:30 到 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);
【问题讨论】:
@radashk sale_id 是主索引,并链接到另一个名为 sales 的表,其中包含每张收据销售的产品。有些收据销售了 3 件产品,而其他收据只有一件,所以我将其分离到一个一对多的关系数据库中。 索引时间列。我没有看到其他任何重大改进 我在这里有一个答案:***.com/a/11367541/9094,它允许您根据任意时间间隔对组进行查询,您可以采用它来满足您的需求。 @radashk 我按照你的建议索引了时间 ID,查询速度现在为 1.5 秒。很棒的东西伙计。虽然您的解决方案很棒,而且我并不是要减少您的帮助和专业知识,但我希望有一个可以仅通过 SQL 完成的解决方案。 如果今晚晚些时候我有时间,我会整理一个详细的潜在解决方案,但简而言之:不要将日期和时间字段分开,使用单个字段。 Unix 时间戳或 DATETIME 无关紧要。这使事情更容易走出大门。您应该在单个查询中获取所有数据,让 mysql 将其分成组而不是使用 PHP。使用日期/时间函数将您的日期时间字段转换为小时和分钟,然后在这些字段上使用GROUP BY
将小时/半小时组合在一起并生成总和。
【参考方案1】:
您也可以尝试这个单一查询,它应该返回一个包含 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 |
| ...
+---------------------+--------------------+
【讨论】:
您的查询假定 MySQL 选择的time
值将是最小时间,这假定按时间顺序升序排序的自然表顺序,这可能不是真的。您应该在 SELECT 子句中使用 min(time
)。
@greenlion 谢谢,我将其编辑为在时间列上使用MIN
。
@greenlion 良好的通话,返回的结果在适当的时间间隔内。
@drew010 感谢这次抽奖,到目前为止,这个解决方案是我的首选。
@PontusTrade 谢谢,我在我的一张桌子上测试了它,它有一个DATETIME
列,这反映在我的输出中。显然,您在time
列中的结果将只是时间。我刚刚编辑了查询,也选择了仅供参考的日期。【参考方案2】:
首先,我会使用单个 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;
【讨论】:
我得到了一个奇怪的结果。它以每日间隔输出小计的总和。在此处粘贴数组返回:pastebin.com/16QPzj5T 您是否不小心使用了“时间”(单引号)而不是time
(反引号)?看起来 min(time), max(time) 正在返回文字值“time”。
@greenlion 哎呀,我使用带引号的时间而不是单独使用时间。这是更新的数组转储。我可能做错了什么,但结果似乎是给定时间范围内每天的小计。 pastebin.com/pM0mPrmr
请发布您运行结果的确切 SQL。谢谢。【参考方案3】:
更新:
由于您不关心任何“丢失”的行,我还将假设(可能是错误的)您不关心查询可能会返回上午 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 子句。
WHERE r.date = '2011-09-10'
覆盖索引ON Receipts(time,subtotal)
(如果您还没有覆盖索引)可能有助于提高性能。 (如果您在日期列上包含相等谓词(如上面的 WHERE 子句中,最合适的覆盖索引可能是 ON Receipts(date,time,subtotal)
。
我假设time
列的数据类型为TIME。 (如果不是,则可能需要对查询(在别名为 t
的内联视图中)进行小幅调整,以使(派生的)b_time 和 e_time 列的数据类型与 @987654337 的数据类型匹配收据中的@列。
如果在给定时间段内 Receipts 中没有行,则不能保证返回 33 行。 “缺失行”对您来说可能不是问题,但它是时间序列和时间段数据的常见问题。
我假设您希望保证返回 33 行。当没有找到与时间段匹配的行时,上面的查询返回零的小计。 (我注意到,在这种情况下,您当前的解决方案将返回 NULL。我已经将该 SUM 聚合包装在 IFNULL 函数中,这样当 SUM 为 NULL 时它将返回 0。)
因此,别名为t
的内联查询是一个丑陋的混乱,但它运行得很快。它所做的是生成 33 行,具有不同的整数值 0 到 33。同时,它派生一个“开始时间”和一个“结束时间”,用于将每个周期“匹配”到 time
列在Receipts
表上。
我们注意不要将 Receipts 表中的 time
列包装在任何函数中,而只引用裸列。而且我们要确保没有任何隐式转换正在进行(这就是为什么我们希望 b_time 和 e__time 的数据类型匹配。ADDTIME
和 SEC_TO_TIME
函数都返回 TIME
数据类型。(我们可以'无需进行匹配和 GROUP BY 操作。)
最后一个时段的“结束时间”值返回为“24:00:00”,我们通过运行此测试验证这是一个有效的匹配时间:
SELECT MAKETIME(23,59,59) < MAKETIME(24,0,0)
这是成功的(返回 1)所以我们很好。
派生列(t.b_time
和 t.e_time
)也可以包含在结果集中,但创建数组时不需要它们,如果不包含它们(可能)效率更高。
最后一点:为了获得最佳性能,将别名为 t
的内联视图加载到实际表中可能会有所帮助(临时表也可以。),然后您可以引用该表来代替内联视图。这样做的好处是您可以在该表上创建索引。
【讨论】:
嗨斯宾塞。您的解决方案返回我需要的确切结果,并且与我当前循环的方式相同。我唯一担心的是,运行整个脚本仍然需要 9 秒,而 draw at ***.com/a/11768140/1547373 提供的解决方案需要半秒。也许我错过了为什么要花这么长时间?在没有时间的情况下丢失行也是一个好点。有足够的数据,所以它不会成为问题。我想奖励这两个答案都是正确的,因为它们都是正确的,只是方式不同。 您好,本都。 9 秒的经过时间比我预期的要长得多。 Drew 的解决方案涉及日期,但我的解决方案没有。性能肯定会更快,但我真的需要查看 EXPLAIN 和表定义,包括索引。我提供的查询将处理这 150,000 行中的每一行,并将它们全部分配到 33 个“桶”中。由于此查询中的 JOIN 操作,它将受益于适当的索引。 @Pontus:我更新了我的答案以添加一个查询,该查询应该比我之前的答案更有效地返回指定的结果。这将返回两列:i(作为问题中所示的期间标识符和小计。【参考方案4】:使其成为纯 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
【讨论】:
【参考方案5】:在我的查询中,我假设有一个名为 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
【讨论】:
【参考方案6】:始终为您的数据使用正确的数据类型。对于您的日期/时间列,最好将它们存储为(最好是 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
)
【讨论】:
【参考方案7】:我也找到了一个不同的解决方案,并在此处发布以供任何人偶然发现此问题时参考。按半小时间隔分组。
SELECT SUM(total), time, date
FROM tableName
GROUP BY (2*HOUR(time) + FLOOR(MINUTE(time)/30))
更多信息的链接 http://www.artfulsoftware.com/infotree/queries.php#106
【讨论】:
以上是关于如何高效地使用 SQL 检索半小时间隔的数据?的主要内容,如果未能解决你的问题,请参考以下文章