设为首页 加入收藏

TOP

锋利的SQL-基于窗口的排名计算
2015-07-24 10:16:03 来源: 作者: 【 】 浏览:0
Tags:锋利 SQL- 基于 窗口 排名 计算

在SQL Server中,窗口被定义为用户指定的一组行。

之所以要提出窗口这个概念,因为这种基于窗口或分区的重新计算在实际工作应用范围比较广泛。例如,假设我们要对每个班级中的学生按成绩进行排序,在对第1个班级排序完成后,对第2个班级进行排序时编号需要重新从1开始。在SQL Server 2005之前,像这种排序方式实现起来是比较烦琐的。可以说,对新窗口重新启动计算是窗口计算的重要特点。

为支持窗口计算,SQLServer提供了OVER子句和窗口函数。窗口函数在MSDN Library中被翻译为开窗函数。虽然“开窗函数”理解起来并不如“窗口函数”容易,但是它描述了数据窗口变化后重新启动计算这样一个动作,所以我们尊重MSDN Library中的翻译,在后续的介绍中将使用“开窗函数”这一名词。

窗口计算的两个主要应用就是对每组内的数据进行排序和聚合计算。因此,开窗函数也被分为排名开窗函数和聚合开窗函数。排名开窗函数如ROW_NUMBER( )、RANK( ),聚合开窗函数如AVG( )、SUM等。

进行排名计算时,OVER子句的语法格式如下:

OVER ( [ PARTITION BY value_expression , ... [ n ]]

)

PARTITION BY value_expression

指定对相应FROM子句生成的行集进行分区所依据的列。开窗函数分别应用于每个分区,并为每个分区重新启动计算。value_expression只能引用通过FROM子句可用的列,不能引用选择列表中的表达式或别名。value_expression可以是列表达式、标量子查询、标量函数或用户定义的变量。

指定应用排名开窗函数的排序顺序。只能引用通过FROM子句可用的列,但是不同通过指定整数来表示选择列表中列名称或列别名的位置。

下面我们将以表9-1所示的Students表为例,进行介绍。像Students表这样的数据结构设计,相对于数据库存储而言是比较合理的,因为我们不可能为每个班级创建一个表,但确实又存在像为每个班级中的学生成绩进行排序或为学生编号这样的实际需求,SQL Server的窗口计算技术就有效解决了二者之间的矛盾。

从SQL Server2005开始,提供了4个排名函数,分别是:ROW_NUMBER( )、RANK( )、DENSE_RANK( )和NTILE( ),它们可以为分区中的每一行返回一个排名值。ROW_NUMBER( )用于按行进行编号,RANK( )和DENSE_RANK( )用于按指定顺序排名,NTILE( )用于对数据进行分区。

9.2.1 ROW_NUMBER( )

ROW_NUMBER( )返回分区内行的序列号,每个分区的第一行从1开始。例如,下面的语句指定按ClassID进行分区,并按StudentName进行排序编号。查询结果如表9-2所示。

SELECT ClassID, StudentName, Achievement,

ROW_NUMBER() OVER(PARTITION BY ClassID ORDER BY StudentName) ASRowNumber

FROM Students;

表9-2 按班级分区、按学生姓名进行编号

ClassID

StudentName

Achievement

RowNumber

1

Andrew

99.00

1

1

Grace

99.00

2

1

Janet

75.00

3

1

Margaret

89.00

4

2

Michael

72.00

1

2

Robert

91.00

2

2

Steven

86.00

3

3

Ann

94.00

1

3

Ina

80.00

2

3

Ken

92.00

3

3

Laura

75.00

4

为了理解SQL Server中排名函数的工作原理,我们来看一下查询优化器为查询生成的执行计划,如图9-1所示。

\

图9-1 为ROW_NUMBER( )生成的执行计划

由上图可以看出,为了计算排名,优化器首先按分区列排序,然后再对分区内行按ORDER BY子句指定的列排序。如果事先为表创建了符合该排序条件的索引,则会直接扫描该索引文件,不再进行排序。

“序列射影”运算符的工作是负责计算排名,“段”运算符用于确定分组边界。二者相互协调工作,来确定每一行的排名值。

“段”运算符在内存中会保留一行,用来与下一行的PARTITION BY列值进行比较。对于表中的第一行,“段”运算符自然会发送true信号。对于后面的行,直到PARTITIONBY列值有变化之前,会一直发送false信号。如果PARTITION BY列值发生了变化,说明已经到了下一个分区,“段”运算符会再次发送true信号。“序列射影”运算符在接收到true信号后,会重置排名值。<??http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vcD48cD7I57n7obDQ8sHQyeTTsKGx1MvL47f7vdPK1bW9tcTKx2ZhbHNl0MW6xaOsy/y74ci3yM+1scewyuTI69DQtcTFxdDy1rXKx7fxsrvNrNPayc/Su9DQo6zI57n7srvNrKOs1PKwtMXFw/u6r8r9y/nWuMq+tcS13dT2xcXD+9a1oaPX1Mi7o6zU2rjDyr7A/dbQo6zTydPaUk9XX05VTUJFUiggKbqvyv3Q6NKqzqrDv9K70NC13dT21rWho9LytMujrNXiuPbFxdDy1rWxyL3Psr3W6NTauMPKvsD91tDKx7K7tObU2rXEoaO1q8rHo6y21NPaz/FSQU5LKCApus1ERU5TRV9SQU5LKCApuq/K/aOs1NrWtNDQvMa7rtbQu7m74dPQwe3N4tK7uPahsLbOobHUy8vjt/ujrNPD09qxyL3PxcXQ8ta1yse38dPQseS7r6Os0tTIt7aoyse38bXd1PbFxcP71rWho7TLzsrM4s7Sw8fU2s/Cw+a7ubvh09C96cncoaM8L3A+PGgzPjkuMi4yIFJBTksoICm6zURFTlNFX1JBTksoICm6r8r9PC9oMz48cD5ST1dfTlVNQkVSKCApuq/K/dPD09qx4LrFo6zL/NPrxcXD+77f09Cyu82stcS4xcTuoaPA/cjno6zTybHtOS0xv8nS1L+0s/ajrLDgvLYx1tC1xEdyYWNlus1BbmRyZXe1xLPJvKjP4M2so6y2vMrHOTm31qGjyOe5+8q508NST1dfTlVNQkVSKCApuq/K/bHgusWjrNPQwb3W1rHgusW3vbC4v8m5qdGh1PGjutK71tbKx0dyYWNltdoxoaJBbmRyZXe12jKjrMHt0rvW1srHQW5kcmV3tdoxoaJHcmFjZbXaMqGj1eLL5Mi7trzKx9X9yLe1xKOsy/y+39PQsrvIt7ao0NShozwvcD48cD62+MXFw/vU8rK7zazBy6Osy/y+39PQyLe2qNDUo6zP4M2stcTFxdDy1rXX3MrHsbu31sXkz+DNrLXExcXD+9a1oaNHcmFjZbrNQW5kcmV31NrFxcP7tcTH6b/2z8K2vNOmtbHKx7XaMaOs0rK+zcrHztLDx7Ojy7W1xLKiwdC12jGho8THy/vDx8G9yMvWrrrztcTD+7TOysfKssO0xNijv8rHtdoyu7nKx7XaM8TYo7+008G9yMuyosHQtdoxtcS9x7bIvbKjrMv7w8fBvcjL1q6687XEw/u0ztOmtbHKx7XaMqOs1eLSssrHREVOU0VfUkFOSygpuq/K/bXExcXD+7e9yr2ju8eww+bS0b6t09AyuPbIyzk5t9bBy6Osy/vDx7rzw+a1xMjL06a1scrHtdozuPa437fW1d+jrLTT1eK49r3HtsjA7b3io6y688PmtcTD+7TO06a1scrHtdozo6zV4tKyysdSQU5LKCAptcTFxcP7t73KvaGjREVOU0VfUkFOSyggKbqvyv21xMXFw/u3vcq9ztLDx7PG1q7OqsPcvK/FxcP7o6zS8s6qy/y1xMP7tM7Wrrzkw7vT0LzkuPShozwvcD48cD7PwsPmtcTT777k0d3KvsHLUkFOSygpus1ERU5TRV9SQU5LKCAptcTFxcP7t73KvaOssunRr73hufvI57HtOS0zy/nKvqGjPC9wPjxwPlNFTEVDVCBDbGFzc0lELCBTdHVkZW50TmFtZSwgQWNoaWV2ZW1lbnQsPC9wPjxwPiAgICAgIFJBTksoKSBPVkVSKFBBUlRJVElPTiBCWSBDbGFzc0lEIE9SREVSIEJZIEFjaGlldmVtZW50IERFU0MpIEFTIFNvcnRSYW5rLDwvcD48cD4gICAgICBERU5TRV9SQU5LKCkgT1ZFUihQQVJUSVRJT04gQlkgQ2xhc3NJRCBPUkRFUiBCWSBBY2hpZXZlbWVudCBERVNDKSBBUyBTb3J0RGVuc2U8L3A+PHA+RlJPTSBTdHVkZW50czsgPC9wPjxwPrHtOS0zICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgsLSw4Ly2us2/vMrUs8m8qLfWsfDKudPDUkFOSyggKbrNREVOU0VfUkFOSyggKcXFw/s8L3A+PHRhYmxlIGJvcmRlcj0="1" cellspacing="0" cellpadding="0">

ClassID

StudentName

Achievement

SortRank

SortDense

1

Grace

99.00

1

1

1

Andrew

99.00

1

1

1

Margaret

89.00

3

2

1

Janet

75.00

4

3

2

Robert

91.00

1

1

2

Steven

86.00

2

2

2

Michael

72.00

3

3

3

Ann

94.00

1

1

3

Ken

92.00

2

2

3

Ina

80.00

3

3

3

Laura

75.00

4

4

下面是为语句生成的执行计划,与ROW_NUMBER( )相比,执行计划中多出了一个“段”运算符。右边段的分组依据是ClassID,左边段的分组依据是ClassID和Achievement,这是多出的“段”。右边的“段”用于分区操作,在到达下一个分区时发送true信号,“序列射影”运算符会重置排名值。而左边的“段”用于比较排序值是否有变化,如果有变化,则通知“序列射影”运算符递增排名值,递增方式则按RANK( )和DENSE_RANK( )函数的规则进行。

\

图9-2 为RANK( )和DENSE_RANK( )生成的执行计划

在SQL Server2005之前,也可以使用子查询的方式实现排名计算。语句的原理就是查询出比当前成绩高的个数,再加上1,就是该成绩的排名。例如,在第1个班级中,比99分高的成绩为0,加上1后,该成绩就是第1名。下面语句的执行结果表9-3所示相同,但是由于对于每个成绩都要执行两次子查询,在性能方面与RANK()和DENSE_RANK( )函数相差很远。

SELECT ClassID, StudentName, Achievement,

(SELECT COUNT(*) FROM Students AS S2

WHERE S2.ClassID = S1.ClassID AND S2.Achievement > S1.Achievement)+1AS SortRank,

(SELECT COUNT(DISTINCT achievement) FROM Students AS S2

WHERE S2.ClassID = S1.ClassID AND S2.Achievement > S1.Achievement)+1AS SortDense

FROM Students AS S1

ORDER BY ClassID, Achievement DESC;

9.2.3 NTILE( )函数

NTILE( )函数用于把行分发到指定数目的组中。各个组有编号,编号从1开始。对于每一个行,NTILE将返回此行所属的组的编号。

NTILE( )函数可以接受一个代表组数量的参数,分组的方式“均分”原则。例如,假设一个表有10行,需要分成2组,则每个组都会有5行。如果表有11行,需要分成3个组,这时候是无法均分的。它分配方法是先得到一个能够整除的基组大小(11/3=3),每组应当分配3行,剩余的2行(11-9)会被再次均分到前面的2组中。

例如,下面的语句指定将Students表按学生成绩划分为3个组,并且Students表恰好也是11行,分组结果如表9-4所示。

SELECT ClassID, StudentName, Achievement,

NTILE(3) OVER(ORDER BY Achievement DESC) AS Tile

FROM Students;

表9-4 分组结果

ClassID

StudentName

Achievement

Tile

1

Grace

99.00

1

1

Andrew

99.00

1

3

Ann

94.00

1

3

Ken

92.00

1

2

Robert

91.00

2

1

Margaret

89.00

2

2

Steven

86.00

2

3

Ina

80.00

2

3

Laura

75.00

3

1

Janet

75.00

3

2

Michael

72.00

3

也可以先分区,再分组。例如,下面的语句将每个班级的成绩划分为高、低两组,查询结果如表9-5所示。可以看出,包含4名学生的班级,每组是2人;包含3名学生的班级,第1组是2人,第2组是1人。

SELECT ClassID, StudentName, Achievement,

CASENTILE(2) OVER(PARTITION BY ClassID ORDER BY Achievement DESC)

WHEN 1 THEN '高'

WHEN 2 THEN '低'

ENDAS Tile

FROM Students;

表9-5 按班级分区再按成绩分组结果

ClassID

StudentName

Achievement

Tile

1

Grace

99.00

1

Andrew

99.00

1

Margaret

89.00

1

Janet

75.00

2

Robert

91.00

2

Steven

86.00

2

Michael

72.00

3

Ann

94.00

3

Ken

92.00

3

Ina

80.00

3

Laura

75.00

】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇SQLServer数据库优化实战(二) 下一篇SqlServer变更数据捕获(CDC)

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容:

·如何理解c语言指针和 (2025-12-27 01:19:11)
·为什么C标准库没有链 (2025-12-27 01:19:08)
·玩转C语言和数据结构 (2025-12-27 01:19:05)
·MySQL 基础入门视频 (2025-12-26 23:20:22)
·小白入门:MySQL超详 (2025-12-26 23:20:19)