直方图是一种统计报告,它可以显示在某个最小值和最大值之间的值的等级或范围内值出现的频率。让我们以学生的考试成绩结果为例来进行说明。假定有 30 个学生参加考试,最低分为 51,最高分为 100。您希望在最低分和最高分之间生成若干个等级,并计算每个等级内的结果数目,从而了解整个班级的分数分布情况。如果您希望生成五个范围大小相似的连续等级,则可得到如下的等级和范围:50 到 60、60 到 70、70 到 80、80 到 90、90 到 100(下限不包括在内,但包括上限)。该直方图包括各个等级以及每个等级内的结果数目。
同样,利用直方图,您可以分析网络中在服务器上设置的性能计数器(例如,CPU 利用率、内存)的值的采样。例如,假设您每隔五分钟就记录某一网络服务器的 CPU 利用率,某天测得的值在 21% 到 100% 之间变动。您可以生成有四个等级的直方图 — 20 到 40、40 到 60、60 到 80、80 到 100 — 从而得出属于每个等级的样本数。如果您的服务器在那一天超负荷地工作,那么大多数样本会属于第四个等级。
生成性能计数器直方图
下面是一个涉及直方图的问题;您可以看看自己是否能在读到我的解决方案之前就解决它。一个已调度的 SQL Server 代理作业将在一个名为 Samples 的表中定期记录某网络服务器的性能计数器的采样。运行 清单 1 显示的代码,创建 Samples 表并向其中填充数据。采样 (dt) 后,该表的每一行都包含度量 ID (measid)、度量值 (value) 和表示表中其他列的筛选列(100 字节)。(例如,该表通常包括一个 serverid 列,这样您可以记录多个服务器的度量样本。考虑到这个问题,在此示例中,让该表只包含一个服务器的数据。)
假设 measid 1 是 CPU 利用率百分比,measid 2 是以兆字节为单位的内存使用量。您的用户需要直方图来帮助他们分析在一段时期内某个度量的性能数据。用户提供了以下参数:等级数 (@numsteps)、度量 ID (@measid) 和日期范围(@fromdt — 包括起始日期,@todt — 不包括终止日期)。您的任务是生成这些给定参数的直方图。请注意,您不必在结果中包括样本数为 0 的等级。例如,假设用户提供了以下参数:
DECLARE @numsteps int, @measid
int, @fromdt datetime, @todt
datetime
SELECT @numsteps=5, @measid=1,
@fromdt='20030101', @todt='20030102'
在 Samples 表中,在给定期间内,measid 1 的最小度量值是 26,最大度量值是 50。在 @numsteps 参数中请求的等级数为 5。首先,您需要计算五个等级内各范围的下限和上限。因为范围的下限不包括在内,所以经过计算可得到以下范围:25 到 30、30 到 35、35 到 40、40 到 45、45 到 50。
您需要编写能告诉您每个等级有多少度量的代码 — 在本例中,您的代码应产生与等级 1 相匹配的一个度量 (26)、与等级 2 相匹配的两个度量(33、35)以及与等级 5 相匹配的两个度量(47、50)。 以下是我针对此问题提供的一些解决方案。
解决方案 1:使用等级表
第一个解决方案涉及到编写一个生成派生表 Steps 的查询,该表包含等级号和每个等级的值的范围。生成这样一个表后,该解决方案就显得简单了。您需要完成的操作是:根据属于等级范围内的 value 列将派生表 Steps 联接到 Samples 表、根据等级号对结果进行分组、计算每组内的行数。这个解决方案中比较困难的地方就是编写生成派生表的查询。要生成等级号,可以使用一个名为 Nums 的辅助表,在该表中填充有一个范围为 1 到 的整数序列。运行 清单 2 中的脚本可创建 Nums 辅助表,并向其中填充 1000 个整数。
要计算每个等级范围的下限和上限,需要交叉连接两个查询的结果:一个是针对 Nums 的查询,以返回等级号,另一个是针对 Samples 的查询,以返回最小度量值和最大度量值。产生的查询可能如 清单3 所示。该查询对每个等级号返回相同的常规最小度量值和最大度量值。这只是该解决方案中的中间结果;稍后您会将常规最小值和最大值与等级号一起使用,以便计算等级的最小值和最大值。
现在,您需要用返回的表达式替换 SELECT 列表中的星号,此外,还需要替换等级号以及等级范围的下限(不包括)和上限(包括)。等级号很简单,是 n。
以下代码是构建计算下限的表达式的基础:
mn + *(n-1) - 1
其中,mn 是常规最小度量值,n 是等级号,step_size 是每个等级所覆盖范围的大小。用于计算 step_size 的表达式如下所示:
(mx-mn+1)/@numsteps
通过将这两个表达式结合在一起,您可以得到
mn + (mx-mn+1)/@numsteps*(n-1) - 1
如果您得到的值范围总是能用等级数来整除,那么,使用上面这个表达式就可以了,但在实际应用中,情况却并不总是如此。下面的表达式使用数值除法和舍入法,适用于不能用等级数整除的值范围:
mn + CAST(round(1.0*(mx-mn+1)/@numsteps*(n-1), 0) AS int) - 1
您可以通过将除法运算的第一个操作数与数值 1.0(读法是一点零)相乘来进行分数除法(而非整数除法),这意味着 SQL Server 不会截断小数部分。然后,您需要对结果进行舍入,以便得到整个范围的上限和下限。之后,SQL Server 将经过舍入的结果转换为整数,以便去掉小数点后没有意义的零。您可以用相似的方式来计算上限:
mn + CAST(round(1.0*
(mx-mn+1)/@numsteps*n, 0)
AS int) - 1
上述表达式与前面的表达式之间的唯一区别是它将等级大小与等级号 (n) 相乘,而不是与 n-1 相乘。要测试计算派生的 Steps 表的完整查询,请运行 清单 4 中的代码;请注意,得到的结果与图 1 相同。
请随便试用此解决方案的代码中的参数,以便测试一下更改等级数对结果会有什么影响,等等。请记住,返回等级表的查询只是该解决方案的一部分。现在,您可以在下面的伪代码中看到缺少的、被表示为 的查询部分:
SELECT step, count(*) AS cnt
FROM Samples JOIN () AS Steps
ON value > f AND value = @fromdt AND dt
如果您像我一样喜欢使用模块化的开发方法来简化代码及其维护,则可以编写一个用户定义函数 (UDF),该函数将等级数、度量 ID 和日期范围作为参数,并返回表 Steps。运行 清单 6 显示的代码来创建 fn_steps 函数。
要测试该函数,请运行下面的代码:
SELECT * FROM fn_steps(1, 5, '20030101', '20030102')
现在,您可以使用 fn_steps 表,而不是派生表 Steps,如 清单 7 中的代码所示。
解决方案 2:实时计算等级号
这个难题还有另一个解决方案,该方案不生成等级表,而是实时计算等级号。利用此解决方案时,您无需使用等级号辅助表。请注意 清单 8 (第 18 页)的标注 A 中的 FROM 子句。您将交叉连接两个派生表 — 一个表名为 S,包含来自 Samples 的、与提供的参数相匹配的行,另一个表名为 R,包含最小度量值和整个范围的大小。下面的表达式(在 SELECT 列表中进行编写)用于计算等级号:
floor((value-mn) / ([url=mailto:1.0*range/@numsteps]1.0*range/@numsteps[/url])) + 1
AS step
此表达式使用的逻辑与您在前面的解决方案中用于计算上限和下限的逻辑很相似。表达式 (value-mn) 可计算出值在范围内的位置,而 ([url=mailto:1.0*range/@numsteps]1.0*range/@numsteps[/url]) 可计算出等级大小。用第二个操作数除以第一个操作数,在结果的基础上再加上 1,就可以得出等级号。既然您已经向每行附加了一个等级号(匹配来自 Samples 表的条件),那么您就可以在名为 RS 的派生表中使用此查询,从而能够按计算出的等级号对结果进行分组。清单 8 显示了完整的解决方案,在该解决方案中,外部查询按等级号对结果进行分组,并计算每个等级的行数。
(