如何有效地使用SQL在半小时间隔内检索数据?

时间:2012-08-01 21:35:39

标签: php mysql sql performance

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

我正在使用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

期望的结果

这样的数组:

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

当前方法

当前的方式使用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;
    }
}

当前输出

输出只是一个数组:

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

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

7 个答案:

答案 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的数据类型匹配。timeADDTIME函数都返回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