查询的一部分是使CPU长时间处于最大化状态,这是GROUP BY子句中的功能以及在这种情况下分组总是需要未索引排序的事实。虽然时间戳字段上的索引将有助于初始过滤器,但必须在过滤器匹配的每一行上执行此操作。加快此步伐将使用更有效的途径来完成Alex所建议的相同工作,但您仍然会遇到很大的效率低下,因为使用查询计划程序的任何功能组合都无法提出它可以通过任何索引得到帮助,因此它必须遍历每一行,然后首先运行函数以计算分组值,然后才可以对数据进行排序并计算所得分组的汇总。
因此,解决方案是通过某种方式使进程组可以使用索引,或者以其他方式消除立即考虑所有匹配行的需要。
您可以为每行维护一个额外的列,其中包含将时间舍入为小时的时间,并为该列编制索引以用于此类查询。这会使您的数据不规范,因此可能会感觉“脏”,但是它比缓存所有聚合以供将来使用(并在更改基本数据时更新该缓存)更有效。额外的列应由触发器维护或作为持久化的计算列,而不是由其他地方的逻辑维护,因为这将保证所有当前和将来可能插入数据或更新时间戳列的位置,或者现有行会在新数据中产生一致的数据柱。您仍然可以获取MIN(时间戳)。查询将以这种方式导致的结果仍然是遍历所有行(显然,这是无法避免的),但是它可以对索引进行排序,在到达分组中的下一个值时,为每个分组输出一行,而不必在执行分组/聚合之前为未索引的排序操作记住整个行集。它也将使用更少的内存,因为它不需要记住先前分组值中的任何行即可处理它正在查看的行或其余行。
该方法消除了对整个结果集在内存中某个地方的查找需求,并对组操作进行了未索引的排序,并将组值的计算从大型查询中删除(将作业移至产生结果的单个INSERT / UPDATE中)。数据),并且应允许此类查询以可接受的方式运行,而无需维护汇总结果的单独存储。
一种没有的方法对数据进行非规范化,但仍需要额外的结构,是使用“时间表”,在这种情况下,您可能考虑的所有时间都每小时包含一行。该表不会占用DB或可观大小的大量空间-可以覆盖一张包含两个日期的一行(小时的开始和结束,例如'2011-01-01 @)的表的100年时间跨度00:00:00.0000','2011-01-01 @ 00:00:59.9997',“ 9997”是DATETIME字段不会舍入到下一秒的最小毫秒数),这两者都是集群主键将占用约14Mb的空间(每行8 + 8字节* 24小时/天* 365.25天/年* 100,加上集群索引树结构的开销,但是开销不会很大) 。
SELECT CONVERT(VARCHAR, [timestamp], 1)+' '+ CAST(DATEPART(Hh,[timestamp]) as VARCHAR) AS TimeStampHour
, MIN([timestamp]) as TimeStamp
, AVG(MyField) As AvgField
FROM TimeRangeByHours tt
INNER JOIN MyData md ON md.TimeStamp BETWEEN tt.StartTime AND tt.EndTime
WHERE tt.StartTime > '4/10/2011'
GROUP BY tt.StartTime
ORDER BY tt.StartTime
这意味着查询计划者可以安排使用MyData.TimeStamp上的索引。查询计划程序应该足够聪明,可以与MyData.TimeStamp上的索引一起步入驯服表,再次对每个分组输出一行,并在到达下一个分组值时丢弃每组或每行。不将所有中间行存储在RAM中的某个位置,然后对它们执行未索引的排序。当然,此方法要求您创建时间表,并确保它前后跨得足够远,但是您可以将时间表用于对不同查询中许多日期字段的查询,其中“额外列”选项将需要您需要通过这种方式对每个日期字段进行过滤/分组的额外计算列,以及表格的小尺寸(除非您需要跨度为10,
与您当前的情况和所计算的列解决方案相比,时间表方法有一个额外的差异(这可能是非常有利的):只需更改上面示例查询中的INNER JOIN,就可以返回没有数据的期间的行成为外面的一员。
有人建议没有物理时间表,而总是从表返回函数中返回它。这意味着时间表的内容永远不会存储在磁盘上(或需要从磁盘上读取),并且如果函数编写得当,您就不必担心时间表需要在时间上来回移动多长时间。怀疑为每个查询生成一些行的内存表的CPU成本值得节省一下创建物理时间表的麻烦(如果需要维护,如果其时间跨度要超出初始版本的限制,则可以节省一些开销)。
注意:您在原始查询中也不需要DISTINCT子句。分组将确保这些查询在所考虑的每个时间段内仅返回一行,因此DISTINCT只会做更多事情,而只会旋转CPU多一点(除非查询计划者注意到,distinct是无操作的,否则它将忽略它,不占用额外的CPU时间)。