如何高效地使用 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 &gt;= 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 的数据类型匹配。ADDTIMESEC_TO_TIME 函数都返回 TIME 数据类型。(我们可以'无需进行匹配和 GROUP BY 操作。)

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

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

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

派生列(t.b_timet.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 &gt;= '07:00:00' AND time &lt; '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 检索半小时间隔的数据?的主要内容,如果未能解决你的问题,请参考以下文章

Mysql 每小时平均,间隔从半小时开始

优化慢 SQL 查询

SQL Server - 如何根据将插入数据半小时的表中的几个参数来计算持续时间?

oracle语句 根据操作时间分组

SQL数据库内表太多,查询一次要半个多小时,如何优化?

如何高效地存储与检索大规模的图谱数据?