动态sql的魔鬼和天使
2008-02-02 15:04
316 查看
本文是对http://www.sommarskog.se/其中3片文章的浅显总结
掺杂了不少个人实践中得到的观点
希望能给一些人带来帮助
有问题欢迎提出
经常使用动态的情况是:
declare@sqlnvarchar(100)
set@sql=N'select*fromtblListwhereList_FileNamelike'+@id
declare@sqlnvarchar(100)
set@sql=N'select*fromtblListwhereList_FileNamelike@id'
declare@sqlnvarchar(100)
set@sql=N'select*fromtblListwhereList_FileNamelike'+quotename(@id,'''')
declare@sqlnvarchar(100)
set@sql=N'select*from'+quotename(@id)
如果希望这样的话,放弃吧.
l…orderby@colName可以用case代替
lSelecttop@numberfrom…
Server2005可以直接参数化只用加个括号,selecttop(@topnumber)…
下面介绍后两项的解决方案
图中的红色就是需要根据自己的数据类型更改的.
这样就不需要只要碰到in()参数化,就要用动态sql了.
上述方法存在一个问题,
如果遇到连续的逗号,,就完了
而且返回的列的类型是nvarchar()还是varchar()需要考虑好.否则会带来效率上的问题
因为nvarchar()级高于varchar()
有时候你会处理这样的表
固然这是一个很烂的表,
但是为了查询,你可能需要换成
A200blue
A200green
A200magenta
….
Server2005有个新办法apply
如果使用静态的呢,也不是没办法:
方法一X=@xor@xisnull
掺杂了不少个人实践中得到的观点
希望能给一些人带来帮助
有问题欢迎提出
经常使用动态的情况是:
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()括号内使用函数.如 |
注意: | 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比较好,尽可能减少判断的次数.
相关文章推荐
- 关于动态SQL的使用
- sql语句根据月份动态的求平均数
- sql行列转换例子(动态)
- 动态 SQL
- MySQL中根据if标签实现多条件模糊查询(动态SQL语句)
- mybatis动态sql
- 写MySQL存储过程实现动态执行SQL (转)
- mybatis动态sql中的trim标签的使用
- mybatis动态sql中的trim标签的使用
- MyBatis总结——动态SQL
- MyBatis 官方文档学习3---动态 SQL
- 第一个文章,今天比较兴奋啊! 给大家一个关于SQL复合查询的文章(动态生成多个where条件)
- 在Delphi中动态地使用SQL查询语句 Adoquery sql 参数 冒号
- 存储过程中执行动态Sql语句
- Mybatis动态sql中foreach需要注意的地方
- Mybatis_01_理解动态sql及sql片段
- 三、动态SQL语句
- 自定义ORMapping—动态生成SQL语句
- SQL 查询中动态使用列名(和循环)
- MyBatis参数传入集合之foreach动态sql