您的位置:首页 > 数据库

动态sql的魔鬼和天使

2008-02-02 15:04 316 查看
本文是对http://www.sommarskog.se/其中3片文章的浅显总结
掺杂了不少个人实践中得到的观点
希望能给一些人带来帮助
有问题欢迎提出
经常使用动态的情况是:
SELECT*FROM@tablename

SELECT@colnameFROMtbl

SELECT*FROMtblWHERExIN(@list)

但是这的确是最差劲的方法.


使用动态sql不仅带来了

安全上的问题,比如sql注入,以及对表的权限的设置困难

而且返回结果集也会带来困难


当然,有问题就有解决问题的办法

比如使用限制权限,使用quotename()函数解决注入的问题

使用随机临时表解决数据集的问题


动态SQL优势呢?

对于动态查询尤其有用,比如过滤条件都是可选的情况

有时候是必须用,

比如按表名查询(但是往往这都是因为表的设计不合理造成的),

按列名查询其实就不必使用了,因为可以作为where条件来用.


但是对于除了动态查询的其他情况,怎样避免使用动态sql的麻烦呢?

比如in(@str)的情况

那就是使用表的join操作.这里有个很有用的返回表值得函数.



下面详细叙述:

动态sql的使用:


Sp_executesql

Exec()

优势

可以使用output参数导出对象

没有长度限制

不足

有长度的限制,除非使用nvarchar(max)

不方便导出变量

(后来了解到使用insert表名或临时表exec(@sql)可以导出.)

不能在exec()括号内使用函数.如quotename()

注意:

Execsp_executesql

@sql,

@sql中的参数

@传入(出)的参数

这里的第二项用处不是太大.尤其表名,列名更是不能在这里设置.而要在@sql中就连接好.


l执行的语句中的变量都只在这个语句块中有效.

l如果不是执行一个nvarchar()的变量,而是一个单引号的字符串(被当作varchar)的话,会造成错误提示,所以要在单引号边上加上N,成为unicode.

l避免引号的问题使用quotename(),避免全球语言的问题使用N’’.但是quotename(,’’’’)会把字符串两端的空格删除.

l查询的表名前面最好加上dbo.对提高效率有帮助,尤其在sql2000里,不加的话它会先查询用户数据库,再查询dbo.

l在单引号字串中的单引号要用两个单引号表示:’’’’表示一个单引号.

lPrint执行的sql以便于查错

l不要使用select*要明确列名.




Sql注入

(参数化可以完全解决注入,以及引号问题,

关键是动态sql要防范这2个问题.

尤其在动态sql的参数方面注意就可以了.参数要按列分别对待.否则不容易避免注入.


如果不使用quotename(),那么对单引号输入就会报错,从而让别有目的的人知道你的sql语句结构.

但是这个不能从根本上解决注入问题.

进而可以加双短线--把你以后的句子注释掉


不仅输入框可以注入,url地址,cookies也是攻击的武器.


方法:

给予访问者适当的权限,不要太大.

不要让iis报明确的错误给用户.


用sp_executesql不用exec().因为前者可以参数化sql语句.

declare@sqlnvarchar(100)
set@sql=N'select*fromtblListwhereList_FileNamelike'+@id
execsp_executesql@sql


当输入:@id=N'75;select*fromtblDic'

则会把tblDic选出来.

如是delete呢?后果不堪设想.


可行的方法有两个

一是参数化

declare@sqlnvarchar(100)
set@sql=N'select*fromtblListwhereList_FileNamelike@id'
execsp_executesql@sql,N'@idnvarchar(100)',@id

这样输入什么都可以



二是使用quotename(,’’’’)进行字符类的转化.注意,不能是quotename()必须是quotename(,’’’’)

否则就转化成列名了.

declare@sqlnvarchar(100)
set@sql=N'select*fromtblListwhereList_FileNamelike'+quotename(@id,'''')
execsp_executesql@sql


但是这第一个解决方案有个弊端,

就是对于表名或者列名,它们是不能参数化的.而只能字符串组合构成sql句.解决方法就只能是第二个了.

declare@sqlnvarchar(100)
set@sql=N'select*from'+quotename(@id)
execsp_executesql@sql

quotename()

而第二个使用quotename()也并非完美

只能处理nvarchar(128)以内的,长度超过128就会报错了,那么用个自定义的吧

CREATEFUNCTIONquotestring(@strnvarchar(1998))RETURNSnvarchar(4000)AS

BEGIN

DECLARE@retnvarchar(4000),

@sqchar(1)

SELECT@sq=''''

SELECT@ret=replace(@str,@sq,@sq+@sq)

RETURN(@sq+@ret+@sq)

END


务必注意的是,quotename()在表名是动态的时候使用,

因为相当于加上了[],如果用quotename(,’’’’)那么就相当于一个常数串了,显然不行.

同样的,

如果你的字串是对列的操作,比如某个varchar型的列等于几:col=quotename(,’’’’),这样就可以把sql句变成

Col=‘abc…’,显然是没有错误的

而,如果是某个非以上意义的串,

比如对’select*’这个字串使用quotename(,’’’’)那么就只能等待报错了.因为你得到的是

‘select*’from….引号成为了多余.

动态sql不能用于用户定义函数

自定义函数不能更改表的状态.
如果希望这样的话,放弃吧.

动态游标

利用Output参数

DECLARE@my_curCURSOR

EXECsp_executesql

N'SET@my_cur=CURSORSTATICFOR

SELECTnameFROMdbo.sysobjects;

OPEN@my_cur',

N'@my_curcursorOUTPUT',@my_curOUTPUT

FETCHNEXTFROM@my_cur


看似必须用动态sql的情况

lSelect*from@tblName最好不要用,肯定是数据库设计错误才会把表名当错误.
l…orderby@colName可以用case代替
SELECTcol1,col2,col3

FROMdbo.tbl

ORDERBYCASE@col1

WHEN'col1'THENcol1

WHEN'col2'THENcol2

WHEN'col3'THENcol3

END


但,如果col1,2,3是不同数据类型的列,那么就要换个样子了

SELECTcol1,col2,col3

FROMdbo.tbl

ORDERBYCASE@col1WHEN'col1'THENcol1ELSENULLEND,

CASE@col1WHEN'col2'THENcol2ELSENULLEND,

CASE@col1WHEN'col3'THENcol3ELSENULLEND


lSelecttop@numberfrom…
Server2005可以直接参数化只用加个括号,selecttop(@topnumber)…
对于2000,

CREATEPROCEDUREget_first_n@nintAS

SETROWCOUNT@n

SELECTau_id,au_lname,au_fname

FROMauthors

ORDERBYau_id

SETROWCOUNT0

注意,这里不是setrowcount=@n

而是setrowcount@n

Rowcount不同于@@rowcount,一个是输入值,一个是输出值.


lSelect*fromtblwhere@conditions

除了动态查询有这个必要.


lSELECT*FROMtblWHEREcolIN(@list)

下面介绍后两项的解决方案

In(@filter)

还是先迫不及待的宣告这个方法
CREATEFUNCTIONiter$simple_intlist_to_tbl(@listnvarchar(MAX))

RETURNS@tblTABLE(numberintNOTNULL)AS

BEGIN

DECLARE@posint,

@nextposint,

@valuelenint


SELECT@pos=0,@nextpos=1


WHILE@nextpos>0

BEGIN

SELECT@nextpos=charindex(',',@list,@pos+1)

SELECT@valuelen=CASEWHEN@nextpos>0

THEN@nextpos

ELSElen(@list)+1

END-@pos-1

INSERT@tbl(number)

VALUES(convert(int,substring(@list,@pos+1,@valuelen)))

SELECT@pos=@nextpos

END

RETURN

END


图中的红色就是需要根据自己的数据类型更改的.

CREATEPROCEDUREget_product_names_iter@idsvarchar(50)AS

SELECTP.ProductName,P.ProductID

FROMNorthwind..ProductsP

JOINiter$simple_intlist_to_tbl(@ids)iONP.ProductID=i.number

go

EXECget_product_names_iter'9,12,27,37'


这样就不需要只要碰到in()参数化,就要用动态sql了.

上述方法存在一个问题,
如果遇到连续的逗号,,就完了
而且返回的列的类型是nvarchar()还是varchar()需要考虑好.否则会带来效率上的问题
因为nvarchar()级高于varchar()

有时候你会处理这样的表



固然这是一个很烂的表,
但是为了查询,你可能需要换成
A200blue
A200green
A200magenta
….
Server2005有个新办法apply

SELECTm.modelid,t.strAScolour

FROMmodelsm

CROSSAPPLYiter_charlist_to_tbl(m.colours,',')ASt

ORDERBYm.modelid,t.str


Apply和join很相似,但是join的对象不能是表值函数,而apply可以

Apply也有crossapply和outerapply,含义很明显.


动态查询

使用动态语句:
CREATEPROCEDUREsearch_orders_1--1

@orderidint=NULL,--2

@fromdatedatetime=NULL,--3

@todatedatetime=NULL,--4

--15

DECLARE@sqlnvarchar(4000),--16

@paramlistnvarchar(4000)--17

--18

SELECT@sql=--19

'SELECT..--24

FROMdbo.Orderso--25

JOINdbo.[OrderDetails]odONo.OrderID=od.OrderID--26

JOINdbo.CustomerscONo.CustomerID=c.CustomerID--27

JOINdbo.ProductspONp.ProductID=od.ProductID--28

WHERE1=1'--29

--30

IF@orderidISNOTNULL--31

SELECT@sql=@sql+'ANDo.OrderID=@xorderid'+--32

'ANDod.OrderID=@xorderid'--33

--34

IF@fromdateISNOTNULL--35

SELECT@sql=@sql+'ANDo.OrderDate>=@xfromdate'--36

--37

IF@todateISNOTNULL--38

SELECT@sql=@sql+'ANDo.OrderDate<=@xtodate'--39

--40

--74

SELECT@paramlist='@xorderidint,--75

@xfromdatedatetime,--76

@xtodatedatetime,--77



--87

EXECsp_executesql@sql,@paramlist,--88

@orderid,@fromdate,@todate,@minprice,--89

@maxprice,@custid,@custname,@city,@region,--90

@country,@prodid,@prodname--91


如果使用静态的呢,也不是没办法:
方法一X=@xor@xisnull
(o.OrderID=@orderidOR@orderidISNULL)

AND(o.OrderDate>=@fromdateOR@fromdateISNULL)

AND(o.OrderDate<=@todateOR@todateISNULL)


这样为Null时也成立了.只是只有那些非Null得才有意义.

但是问题是,在caching的情况下,不同的查询之间会相互影响很大;

所以在alterprocedure

参数后面

加上一句话:withrecompile


第二个问题是,引起全表扫描,因为你用的是or.


方法二用coalesce


o.orderID=coalesce(@orderid,o.OrderID)


方法三用between

o.OrderIDBETWEENcoalesce(@orderID,@MinInt)AND

coalesce(@orderID,@MaxInt)


方法四用x=@xand@xisnotnull


总的来说,还是使用动态sql比较好,尽可能减少判断的次数.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: