首页 > 开发 > 综合 > 正文

数据库查询结果的动态排序

2024-07-21 02:06:06
字体:
来源:转载
供稿:网友


在公共新闻组中,一个经常出现的问题是“怎样才能根据传递给存储过程的参数返回一个排序的输出?”。在一些高水平专家的帮助之下,我整理出了这个问题的几种解决方案。

一、用if...else执行预先编写好的查询


  对于大多数人来说,首先想到的做法也许是:通过if...else语句,执行几个预先编写好的查询中的一个。例如,假设要从northwind数据库查询得到一个货主(shipper)的排序列表,发出调用的代码以存储过程参数的形式指定一个列,存储过程根据这个列排序输出结果。listing 1显示了这种存储过程的一个可能的实现(getsortedshippers存储过程)。


【listing 1: 用if...else执行多个预先编写好的查询中的一个】


create proc getsortedshippers

@ordseq as int

as


if @ordseq = 1

select * from shippers order by shipperid

else if @ordseq = 2

select * from shippers order by companyname

else if @ordseq = 3

select * from shippers order by phone



  这种方法的优点是代码很简单、很容易理解,sql server的查询优化器能够为每一个select查询创建一个查询优化计划,确保代码具有最优的性能。这种方法最主要的缺点是,如果查询的要求发生了改变,你必须修改多个独立的select查询——在这里是三个。


二、用列名字作为参数

  另外一个选择是让查询以参数的形式接收一个列名字。listing 2显示了修改后的getsortedshippers存储过程。case表达式根据接收到的参数,确定sql server在order by子句中使用哪一个列值。注意,order by子句中的表达式并未在select清单中出现。在ansi sql-92标准中,order by子句中不允许出现没有在select清单中指定的表达式,但ansi sql-99标准允许。sql server一直允许这种用法。


【listing 2:用列名字作为参数,第一次尝试】


create proc getsortedshippers

@colname as sysname

as


select *

from shippers

order by

case @colname

when 'shipperid' then shipperid

when 'companyname' then companyname

when 'phone' then phone

else null

end



  现在,我们来试一下新的存储过程,以参数的形式指定shipperid列:


exec getsortedshippers 'shipperid'



  此时一切正常。但是,当我们视图把companyname列作为参数调用存储过程时,它不再有效:


exec getsortedshippers 'companyname'



  仔细看一下错误信息:


server: msg 245, level 16, state 1, procedure getsortedshippers, line 5

syntax error converting the nvarchar value 'speedy

express' to a column of data type int.



  它显示出,sql server试图把“speedy express”(nvarchar数据类型)转换成一个整数值——当然,这个操作是不可能成功的。出现错误的原因在于,按照“数据类型优先级”规则,case表示式中最高优先级的数据类型决定了表达式返回值的数据类型。“数据类型优先级”规则可以在sql server books online(bol)找到,它规定了int数据类型的优先级要比nvarchar数据类型高。前面的代码要求sql server按照companyname排序输出,companyname是nvarchar数据类型。这个case表达式的返回值可能是shipperid(int类型),可能是companyname(nvarchar类型),或phone(nvarchar类型)。由于int类型具有较高的优先级,因此case表达式返回值的数据类型应该是int。


为了避免出现这种转换错误,我们可以尝试把shipperid转换成varchar数据类型。采用这种方法之后,nvarchar将作为最高优先级的数据类型被返回。listing 3显示了修改后的getsortedshippers存储过程。

【listing 3:用列名字作为参数,第二次尝试】


alter proc getsortedshippers

@colname as sysname

as


select *

from shippers

order by

case @colname

when 'shipperid'

then cast(shipperid as varchar(11))

when 'companyname'

then companyname

when 'phone'

then phone

else null

end



  现在,假设我们再把三个列名字中的任意一个作为参数调用存储过程,输出结果看起来正确。看起来就象指定的列正确地为查询输出提供了排序标准。但这个表只有三个货主,它们的id分别是1、2、3。假设我们把更多的货主加入到表,如listing 4所示(shipperid列有identity属性,sql server自动为该列生成值)。


【listing 4:向shippers表插入一些记录】


insert into shippers values('shipper4', '(111) 222-9999')

insert into shippers values('shipper5', '(111) 222-8888')

insert into shippers values('shipper6', '(111) 222-7777')

insert into shippers values('shipper7', '(111) 222-6666')

insert into shippers values('shipper8', '(111) 222-5555')

insert into shippers values('shipper9', '(111) 222-4444')

insert into shippers values('shipper10', '(111) 222-3333')



  现在调用存储过程,指定shipperid作为排序列:


exec getsortedshippers 'shipperid'



  表一显示了存储过程的输出。shipperid等于10的记录位置错误,因为这个存储过程的排序输出是字符排序,而不是整数排序。按照字符排序时,10排列在2的前面,因为10的开始字符是1。


表一:记录排序错误的查询结果


shipperid companyname phone

1 speedy express (503) 555-9831

10 shipper10 (111) 222-3333

2 united package (503) 555-3199

3 federal shipping (503) 555-9931

4 shipper4 (111) 222-9999

5 shipper5 (111) 222-8888

6 shipper6 (111) 222-7777

7 shipper7 (111) 222-6666

8 shipper8 (111) 222-5555

9 shipper9 (111) 222-4444


为了解决这个问题,我们可以用前置的0补足shipperid值,使得shipperid值都有同样的长度。按照这种方法,基于字符的排序具有和整数排序同样的输出结果。修改后的存储过程如listing 5所示。十个0被置于shipperid的绝对值之前,而在结果中,代码只是使用最右边的10个字符。sign函数确定在正数的前面加上加号(+)前缀,还是在负数的前面加上负号(-)前缀。按照这种方法,输出结果总是有11个字符,包含一个“+”或“-”字符、前导的字符0以及shipperid的绝对值。

【listing 5:用列名字作为参数,第三次尝试】


alter proc getsortedshippers

@colname as sysname

as


select *

from shippers

order by

case @colname

when 'shipperid' then case sign(shipperid)

when -1 then '-'

when 0 then '+'

when 1 then '+'

else null

end +

right(replicate('0', 10) +

cast(abs(shipperid) as varchar(10)), 10)

when 'companyname' then companyname

when 'phone' then phone

else null

end



  如果shipperid的值都是正数,加上符号前缀就没有必要,但为了让方案适用于尽可能多的范围,本例加上了符号前缀。排序时“-”在“+”的前面,所以它可以用于正、负数混杂排序的情况。


  现在,如果我们用任意三个列名字之一作为参数调用存储过程,存储过程都能够正确地返回结果。richard romley提出了一种巧妙的处理方法,如listing 6所示。它不再要求我们搞清楚可能涉及的列数据类型。这种方法把order by子句分成三个独立的case表达式,每一个表达式处理一个不同的列,避免了由于case只返回一种特定数据类型的能力而导致的问题。


【listing 6:用列名字作为参数,romley提出的方法】


alter proc getsortedshippers

@colname as sysname

as


select *

from shippers

order by

case @colname when 'shipperid'

then shipperid else null end,

case @colname when 'companyname'

then companyname else null end,

case @colname when 'phone'

then phone else null end



  按照这种方法编写代码,sql server能够为每一个case表达式返回恰当的数据类型,而且无需进行数据类型转换。但应该注意的是,只有当指定的列不需要进行计算时,索引才能够优化排序操作。


三、用列号作为参数

  就象第一个方案所显示地那样,你也许更喜欢用列的编号作为参数,而不是使用列的名字(列的编号即一个代表你想要作为排序依据的列的数字)。这种方法的基本思想与使用列名字作为参数的思想一样:case表达式根据指定的列号确定使用哪一个列进行排序。listing 7显示了修改后的getsortedshippers存储过程。


【listing 7:用列号作为参数】


alter proc getsortedshippers

@colnumber as int

as


select *

from shippers

order by

case @colnumber

when 1 then case sign(shipperid)

when -1 then '-'

when 0 then '+'

when 1 then '+'

else null

end +

right(replicate('0', 10) +

cast(abs(shipperid) as varchar(10)), 10)

when 2 then companyname

when 3 then phone

else null

end



  当然,在这里你也可以使用richard的方法,避免order by子句中列数据类型带来的问题。如果要根据shipperid排序输出,你可以按照下面的方式调用修改后的getsortedshippers存储过程:


exec getsortedshippers 1


四、动态执行

  使用动态执行技术,我们能够更轻松地编写出getsortedshippers存储过程。使用这种方法时,我们只需动态地构造出select语句,然后用exec()命令执行这个select语句。假设传递给存储过程的参数是列的名字,存储过程可以大大缩短:


alter proc getsortedshippers

@colname as sysname

as

exec('select * from shippers order by ' +

@colname)



  在sql server 2000和7.0中,你可以用系统存储过程sp_executesql替代exec()命令。bol说明了使用sp_executesql比使用exec()命令更有利的地方。一般地,如果满足以下三个条件,你能够在不授予存储过程所涉及对象权限的情况下,授予执行存储过程的权限:首先,只使用data manipulation language(dml)语言(即select,insert,update,delete);其次,所有被引用的对象都有与存储过程同样的所有者;第三,没有使用动态命令。


  上面的存储过程不能满足第三个条件。在这种情况下,你必须为所有需要使用存储过程的用户和组显式地授予shippers表的select权限。如果这一点可以接受的话,一切不存在问题。类似地,你可以修改存储过程,使它接受一个列号参数,如listing 8所示。


【listing 8:用列号作为参数,动态执行(代码较长的方法)】


alter proc getsortedshippers

@colnumber as int

as


declare @cmd as varchar(8000)


set @cmd = 'select * from shippers order by ' +

case @colnumber

when 1 then 'shipperid'

when 2 then 'companyname'

when 3 then 'phone'

else 'null'

end


exec(@cmd)



  注意,当你使用了函数时,你应该在一个变量而不是exec()命令内构造select语句。此时,case表达式动态地确定使用哪一个列。还有一种更简短的格式,t-sql允许在order by子句中指定select清单中列的位置,如listing 9所示。这种格式遵从了sql-92标准,但ansi sql-99标准不支持这种格式,所以最好不要使用这种格式。


【listing 9:列号作为参数,动态执行(代码较短的方法)】


alter proc getsortedshippers

@colnumber as int

as

declare @cmd as varchar(8000)

set @cmd = 'select * from shippers order by ' + cast(@colnumber as varchar(4))


exec(@cmd)


五、用户定义函数

  如果你使用的是sql server 2000,想要编写一个用户定义的函数(udf),这个用户定义函数接受列的名字或编号为参数、返回排序的结果集,listing 10显示了大多数程序员当成第一选择的方法。


【listing 10:列名字作为参数,使用udf】


create function ufn_getsortedshippers

(

@colname as sysname

)

returns table

as


return

select *

from shippers

order by

case @colname

when 'shipperid' then case sign(shipperid)

when -1 then '-'

when 0 then '+'

when 1 then '+'

else null

end +

right(replicate('0', 10) +

cast(abs(shipperid) as

varchar(10)), 10)

when 'companyname' then companyname

when 'phone' then phone

else null

end



  但是,sql server不接受这个函数,它将返回如下错误信息:


server: msg 1033, level 15, state 1, procedure ufn_getsortedshippers,

line 24

the order by clause is invalid in views, inline functions, and

subqueries, unless top is also specified.



  注意错误信息中的“unless”。sql server 2000不允许在视图、嵌入式udf、子查询中出现order by子句,因为它们都应该返回一个表,表不能指定行的次序。然而,如果使用了top关键词,order by子句将帮助确定查询所返回的行。因此,如果指定了top,你还可以同时指定order by。由于在带有top的udf中允许使用order by子句,你可以使用一个技巧:把“select *”替换成“select top 100 percent *”。这样,你就能够成功地构造出一个接受列名字或编号为参数、返回排序结果的函数。


  新构造的函数可以按照如下方式调用:


select * from ufn_getsortedshippers('shipperid')



  现在,你已经了解了几种用参数确定查询输出中记录次序的方法。在编写那些允许用户指定查询结果排序标准的列的应用程序时,你可以使用本文介绍的各种技术,用列名字或编号作为参数,构造出使用case表达式和动态执行能力的各种方案。
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表