T-SQL动态查询(4)——动态SQL


接上文:T-SQL动态查询(3)——静态SQL前端

 

前言:


前面说了很是多关于动态查询的内容。本文将介绍使用动态SQL解决动态查询的一些方法。sql

 

为何使用动态SQL:


在很是多项目中,动态SQL被普遍使用甚至滥用。很是多时候,动态SQL又确实是解决很是多需求的首选方法。但是假设不合理地使用,会致使性能问题及没法维护。动态SQL尤为本身的优缺点。是否使用需要进行评估分析:数据库

本文出处:http://blog.csdn.net/dba_huangzj/article/details/50202371编程

动态SQL长处:

  • 动态SQL提供了强大的扩展功能,可以应付复杂的需求,即便在需求添加时也能应对,并且不会因为需求的添加而致使代码的线性增加。

  • 运行计划可以缓存查询字符串,意味着大部分查询条件可以重用运行计划缓存而不会致使没必要要的重编译。

动态SQL缺点:

  • 不合理的编码会致使代码的维护陷入困境。

  • 动态SQL是用于应对较高级的问题。对于简单问题,会变得大材小用。
  •  动态SQL的測试显然比其它代码困难,特别是对终于运行的语句的获取,同一时候easy因为编码的不规范致使语法错误。
  • 相对于前面的OPTION(RECOMPILE)。动态SQL需要加入对权限控制的考虑。

  • 动态SQL的计划缓存并不老是你想象的那样。有时候因为输入的參数值而致使不一样的计划生成。

静态SQL事实上可以应对大部分的平常需求。但是随着需求的添加,静态SQL会变得愈来愈复杂,同一时候可能带来过多的重编译,此时应该考虑动态SQL。缓存

 

动态SQL简单介绍:


概述:


在SQL Server中,动态SQL可以由三种方式实现:架构

  1. T-SQL存储过程
  2. CLR存储过程
  3. client语句

本文着重介绍T-SQL中的存储过程。针对用户的输入,有两种方式进行处理:编程语言

  • 把參数经过字符串拼接实现。如:' AND col = ' + convert(varchar, @value)'
  • 使用sp_executesql进行參数化查询。可以把上面的參数变成:' AND col = @value'

基于很是多理由,在平常使用中。推荐使用第二种方法也就是sp_executesql。函数

但是需要提醒的是上面提到的三种实现动态SQL的方式没有本质上的好和坏。仅仅有依据实际状况而定才是最有效的。工具

本文将使用静态SQL篇中的需求做为演示,即针对不一样的查询条件、不一样的排序甚至不一样的汇总需求演示。post

 本文出处:http://blog.csdn.net/dba_huangzj/article/details/50202371

权限:


对于存储过程当中使用静态SQL,权限问题并没有大碍。仅仅要存储过程的调用者和表的拥有者是一样的。因为所有权链(ownership chaining,https://msdn.microsoft.com/zh-cn/library/ms188676.aspx),可以无障碍地运行存储过程。

但是动态SQL中不存在所有权链,即便把它们放在存储过程当中也同样,因为动态SQL有本身的权限范围。

假设在client程序或CLR存储过程当中建立动态SQL,还需要额外授予用户具备查询中涉及到的表、视图、本身定义函数上的SELECT权限。

依据client程序和CLR存储过程的不一样,权限链可能会很是混乱和失控。但是可以使用如下两种方式来应付:

  1. 建立一个证书,对存储过程使用这个证书进行签名。而后使用证书建立一个用户,并因为用户所需的SELECT权限。
  2. 在存储过程当中加入EXECUTE AS ‘用户’。而后授予SELECT权限。

 

动态SQL的參数化查询形式:


本部分使用第一篇中提到的模版进行改造演示。为了能清晰地描写叙述。使用博客自带的行号来标号:

USE [AdventureWorks2008R2]																		
GO                                                                                              
CREATE PROCEDURE [dbo].[sp_Get_orders]                                                          
     @salesorderid    int           = NULL,                                                     
     @fromdate        datetime      = NULL,                                                     
     @todate          datetime      = NULL,                                                     
     @minprice        money         = NULL,                                                     
     @maxprice        money         = NULL,                                                     
     @custid          int           = NULL,                                                     
     @custname        nvarchar(40)  = NULL,                                                     
     @prodid          int           = NULL,                                                     
     @prodname        nvarchar(40)  = NULL,                                                     
     @employeestr     varchar(MAX)  = NULL,                                                     
     @employeetbl     intlist_tbltypeREADONLY,                                                  
     @debug           bit           =0                                                          
AS                                                                                              
                                                                                                
    DECLARE @sql        nvarchar(MAX),                                                          
           @paramlist  nvarchar(4000),                                                          
           @nl         char(2) = char(13) + char(10)                                            
                                                                                                
    SELECT @sql='                                                                               
       SELECT o.SalesOrderID, o.OrderDate, od.UnitPrice,od.OrderQty,                            
             c.CustomerID, per.FirstName as CustomerName,p.ProductID,                           
             p.Name as ProductName, per.BusinessEntityID as EmpolyeeID                          
       FROM  Sales.SalesOrderHeader o                                                           
       INNER JOIN   Sales.SalesOrderDetail od ON o.SalesOrderID= od.SalesOrderID                
       INNER JOIN   Sales.Customer c ON o.CustomerID =c.CustomerID                              
       INNER JOIN   Person.Person per onc.PersonID=per.BusinessEntityID                         
       INNER JOIN   Production.Product p ON p.ProductID =od.ProductID                           
       WHERE 1=1'+@nl                                                                           
                                                                                                
    IF @salesorderidIS NOT NULL                                                                 
       SELECT @sql+= ' AND o.SalesOrderID=@SalesOrderID'+                                       
                     ' ANDod.SalesOrderID=@SalesOrderID'+@nl                                    
                                                                                                
    IF @fromdateIS NOT NULL                                                                     
       SELECT @sql+= ' AND o.OrderDate >= @fromdate'+@nl                                        
                                                                                                
    IF @todateIS NOT NULL                                                                       
       SELECT @sql+= ' AND o.OrderDate <= @todate'+@nl                                          
                                                                                                
    IF @minpriceIS NOT NULL                                                                     
       SELECT @sql += 'AND od.UnitPrice >= @minprice' + @nl                                     
                                                                                                
    IF @maxpriceIS NOT NULL                                                                     
       SELECT @sql += 'AND od.UnitPrice <= @maxprice' + @nl                                     
                                                                                                
    IF @custidIS NOT NULL                                                                       
       SELECT @sql += 'AND o.CustomerID = @custid' +                                            
                    ' AND c.CustomerID = @custid' +@nl                                          
                                                                                                
    IF @custnameIS NOT NULL                                                                     
        SELECT@sql += ' AND per.FirstName LIKE @custname + ''%''' + @nl                         
                                                                                                
    IF @prodidIS NOT NULL                                                                       
       SELECT@sql += ' AND od.ProductID = @prodid' +                                            
                    ' AND p.ProductID = @prodid' +@nl                                           
                                                                                                
    IF @prodnameIS NOT NULL                                                                     
       SELECT@sql += ' AND p.Name LIKE @prodname + ''%''' + @nl                                 
                                                                                                
    IF @employeestrIS NOT NULL                                                                  
       SELECT@sql += ' AND per.BusinessEntityID IN' +                                           
                    ' (SELECT number FROM dbo.intlist_to_tbl(@employeestr))'+ @nl               
                                                                                                
    IF EXISTS(SELECT * FROM @employeetbl)                                                       
       SELECT@sql += ' AND per.BusinessEntityID IN (SELECT val FROM @employeetbl)'+ @nl         
                                                                                                
    SELECT @sql+= ' ORDER BYo.SalesOrderID' + @nl                                               
                                                                                                
    IF @debug= 1                                                                                
       PRINT @sql                                                                               
                                                                                                
    SELECT @paramlist=  '@salesorderid    int,                                                  
                      @fromdate   datetime,                                                     
                      @todate     datetime,                                                     
                      @minprice   money,                                                        
                      @maxprice   money,                                                        
                      @custid     nchar(5),                                                     
                      @custname   nvarchar(40),                                                 
                      @prodid     int,                                                          
                      @prodname   nvarchar(40),                                                 
                      @employeestr varchar(MAX),                                                
                      @employeetbl intlist_tbltype READONLY'                                    
                                                                                                
    EXEC sp_executesql@sql, @paramlist,@salesorderid, @fromdate, @todate, @minprice,            
                   @maxprice,  @custid, @custname,@prodid, @prodname, @employeestr, @employeetbl


 

代码分析:


在上面代码中的第18行。定义了一个变量@sql。用于存储查询字符串。

因为sp_executesql要求參数必须为NVARCHAR,因此这里使用NVARCHAR(MAX),以便足够存放所有终于字符串。

在第20行,使用了一个变量@nl,经过赋值char(13)+char(10)实现Windows上的换行符功能。尽管它是变量,但是在存储过程当中其实是一个常量。

在第22 到31行。包括了动态SQL的核心部分,并存放在@sql变量中。经过兴许的參数拼接实现整个动态SQL查询。

注意代码中均使用了两部命名(即架构名.表名),因为因为性能缘由。SQL Server在编译和优化时需要精肯定位对象,假设表A存在dbo.A和Sales.A这两个架构名,那么SQL Server需要花时间去推断到底使用的是哪一个表,这会带来不小的开销,注意。即便仅仅有几十毫秒,但是对于一个频繁被运行的存储过程或语句,整体性能会被明显拉低,因此不管基于性能仍是编程规范的考虑,都应该带上架构名。固然假设你的系统仅仅有dbo这个默认架构,不带也行,但是建议仍是要规范化编程提升可读性和可维护性。

这里再插一句,在本人优化的代码中。经常看到很是多语句中。表名使用了别名,但是在ON、WHERE中又没有带上别名前缀,咋一看上去很是难知道字段来自于哪一个表,要一个一个相关表去检查。花了不应花的时间,为了维护代码的人,大家便可行好吧。

在第31行是一句“WHERE 1=1”,类似编程语言中的占位符。使WHERE语句即便单独存在也不会报错。如下会介绍为何也要加上@nl。

在第33行開始。针对所有单值查询參数进行检查,假设參数不为NULL,则加入到终于的SQL字符串的相应列中。从这里開始就要注意对单引號、双引號的使用,同一时候留意在每次拼接后面都加上了@nl。

在第67 行,对@employeestr參数进行处理,处理方式和上一篇静态SQL同样。其它剩余部分相对简单,不作过多解释。

在第72行。加入了一个參数@debug。默以为0,当用户调用传入1时,输出SQL字符串,这在调试和检查错误时很是实用,因为动态SQL每每很是难直接从代码中看出终于语句,假设在开发过程没有注意引號、空格、类型转换等问题时,都会在兴许调用过程当中报错。经过@debug參数,可以在未运行语句(即还不至于报错中止以前)就把需要运行的语句打印出来,注意顺序很是重要,假设在运行报错后你再想打印就不必定能打印出来了。

对于差点儿每行后面都加入的@nl。固然是有意图的。假设不加换行符,代码可能会变成单行很是长的字符串,print出来不直观。

甚至看起来很是痛苦,尽管现在有格式化工具,但是不是每次都破解成功,对单串字符串的美化仍是比較浪费时间的。

最后,经过sp_executesql运行SQL字符串,这是一个系统存储过程。需要提供两个固定參数,第一个是SQL字符串,第二个是參数列。这些參数必须是nvarchar类型。

在这个样例中。调用语句在存储过程内部。你也可以在外部调用存储过程。但是需要记住的是动态SQL不能得知不论什么调用參数。

注意存储过程最后的參数列@paramlist,是静态的,也就是參数集是固定的,即便有些參数并不是每次都会使用到。

 

測试:

可以使用如下语句对存储过程进行測试:

EXEC [sp_Get_orders]@salesorderid = 70467
EXEC [sp_Get_orders]@custid  = 30097
EXEC [sp_Get_orders]@prodid  = 936
EXEC [sp_Get_orders]@prodid  = 936, @custname = 'Carol'
EXEC [sp_Get_orders]@fromdate = '2007-11-01 00:00:00.000', @todate = '2008-04-18 00:00:00.000'
EXEC [sp_Get_orders]@employeestr = '20124,759,1865', @custid = 29688
 
DECLARE @tbl intlist_tbltype
INSERT @tbl(val) VALUES(20124),(759),(1865)
EXEC [sp_Get_orders]@employeetbl = @tbl, @custid = 29688
对于这类状况,需要对所有參数进行測试。最好是能知道实际使用中哪些參数的使用频率最高。
本文出处:http://blog.csdn.net/dba_huangzj/article/details/50202371

动态SQL的编译和缓存:


每当用户以一样查询參数集进行调用这个存储过程时,运行计划会被重用。假设调用上一章的存储过程sp_get_orders_1时,如:

EXEC sp_get_orders_1@salesorderid = 70467
EXEC sp_get_orders_1@salesorderid = 70468
EXEC sp_get_orders_1@salesorderid = 70469

因为OPTION(RECOMPILE),因此不缓存不论什么运行计划并且每次都重编译。但是对于本文中的存储过程:

EXEC [sp_Get_orders]@salesorderid = 70467
EXEC [sp_Get_orders]@salesorderid = 70468
EXEC [sp_Get_orders]@salesorderid = 70469

仅仅会针对第一次调用进行编译并缓存运行计划,兴许两次调用将使用第一的运行计划进行直接运行。但是当调用的參数变化时,如:

EXEC [sp_Get_orders]@salesorderid = 70467,@prodid  = 870

会发生新的编译并产生新的缓存条目,但原有的用于查询SalesOrderID的运行计划不受影响。

 

特殊的查询条件:


在上一篇静态SQL中,已经展现了怎样用静态SQL实现某些特殊的查询条件,本部分将演示用动态SQL来完毕这些工做。前面提到过,静态SQL针对简单的查询条件,足以应付自如,但是当需求数量和复杂度逐步添加时。静态SQL将变得不可控。此时就需要考虑动态SQL。

 

数据分布不均的状况:


在很是多系统中,常见的一类状况是,订单表上有一个状态列Status。里面有4个值:N(新订单)、P(处理中)、E(异常订单)、C(已处理订单)。同一时候差点儿99%的数据都是为C。

这样的状况下可以使用对该列中C值的过滤索引/筛选索引(filterindex)来过滤没必要要的数据或需要经常查询的数据。

但是假设在动态SQL中这样写:

IF @status IS NOT NULL
  SELECT @sql += ' AND o.Status = @status'

因为动态SQL的运行计划是针对所有状况进行优化的,因此这样的写法是不会专门针对过滤索引发效,需要额外制定一些操做逻辑来“指示”优化器使用这个过滤索引,如:

IF @status IS NOT NULL
   SELECT @sql += ' AND o.Status = @status' +
                  CASE WHEN @status <> 'C' 
                       THEN ' AND o.Status <> ''C'''
                       ELSE ''
                  END
这样的状况是针对单值參数,假设@status为多值。即用户需要筛选某些类型的数据,则需要按这样的方式加入不少其它的处理逻辑。

本身定义排序:

在动态SQL中,很是常见的应用常见是使用本身定义的排序规则。经过用户前端输入的排序条件进行结果集排序。比方:

@sql += ' ORDER BY ' + @sortcol

这样的写法可以知足多列排序。比方’SalesOrderID, OrderTime Desc’。尽管对于知足功能来讲。已经足够了,但是因为client不知道查询自己。可能致使传入的參数不属于相关的表或其它因素致使报错,特别是ORDER BY在T-SQL的逻辑处理中属于接近最后部分。SELECT语句可能把原始列进行重命名、运算等,致使前端没法得知SELECT的终于列名。另外即便是使用了正确的名字,但是在兴许可能因为表结构的变动、列名变动等因素又带来报错。

这样的状况事实上很是难避免。只是多考虑一下问题可能就没有那么严重,比方可以用如下的方式来预处理:

SELECT @sql += ' ORDER BY ' + 
               CASE @sortcol WHEN 'OrderID'      THEN 'o.OrderID'
                             WHEN 'EmplyoeeID'   THEN 'o.EmployeeID'
                             WHEN 'ProductID'    THEN 'od.ProductID'
                             WHEN 'CustomerName' THEN 'c.CompanyName'
                             WHEN 'ProductName'  THEN 'p.ProductName'
                             ELSE 'o.OrderID'
               END + CASE @isdesc WHEN 0 THEN ' ASC' ELSE ' DESC' END

备用表:


在上一章备用表中。提到了关于不一样參数訪问不一样表的状况。这样的状况在动态SQL中实现也不难,可以把FROM部分改写成:

ROM dbo.' + CASE @ishistoric
                  WHEN 0 THEN 'Orders'
                  WHEN 1 THEN 'HistoricOrders'
             END + ' o
JOIN dbo.' + CASE @ishistoric
                  WHEN 0 THEN '[Order Details]'
                  WHEN 1 THEN 'HistoricOrderDetails'
             END + ' od

但是为了不SQL注入的风险,不建议经过前端程序传入表名,而是传入某些标识參数而后在存储过程内部进行表名选择。

 本文出处:http://blog.csdn.net/dba_huangzj/article/details/50202371

 

缓存问题:


參数化动态SQL的当中一个优点是可以经过计划重用而下降编译次数。但是缓存并不老是好的。比方在上一章基础技能部分提到的:

1. exec sp_Get_orders_1@fromdate='20050701',@todate ='20050701'  
2. exec sp_Get_orders_1@fromdate='20050101',@todate ='20051231'  

尽管參数集一样,但是当值不一样的时候,假设这些不一样的值的数据分布严重不均匀,会致使运行计划没法高效支持所有查询。

这样的状况在动态SQL和静态SQL中都比較常见,如下来介绍一下处理方法:

OPTION(RECOMPILE):

对,你又见到它了。在上面提到的特定状况下,假设查询条件是@fromdate和@todate,加入OPTION(RECOMPILE):

IF (@fromdate IS NOT NULL OR @todate IS NOT NULL)
   SELECT @sql += ' OPTION(RECOMPILE)' + @nl

一般来讲,当你发现查询条件上有合适的索引,并且选择度很是依赖于实际值的输入,那么可以加入OPTION(RECOMPILE),以便你总能经过编译获得关于当前统计信息的最佳运行计划。但是显然这样的方式会加入一部分没必要要的编译。比方两次运行的值全然同样时。依然还会编译。

 

索引提示和其它提示:

有时候可以尝试使用“提示,hints”。可以经过CASE WHEN 推断需要传入什么參数。并且对这些參数额外指定需要走的索引。

但是正如前面提到过的。提示要慎用。特别是索引提示,除非你确保索引名永不变动:

FROM   dbo.Orders o ' + CASE WHEN @custid IS NOT NULL AND
                                  (@fromdate IS NOT NULL OR
                                  @todate IS NOT NULL) 
                             THEN 'WITH (INDEX = CustomerID) '
                             ELSE ''
                         END


第二种提示是使用OPTIMIZE FOR。假设你但愿运行计划老是使用占用最多的状况来编译,比方前面提到的status类型中的C,那么可以加入:

IF @status IS NOT NULL
   @sql += ' OPTION (OPTIMIZE FOR (@status = ''C''))'

假设你不想优化器经过嗅探參数来产生运行计划。可以使用:

IF @fromdate IS NOT NULL AND @todate IS NOT NULL
   @sql += ' OPTION (OPTIMIZE FOR (@fromdate UNKNOWN, @todate UNKNOWN))'

这样优化器就不会使用标准假设,即10%左右来编译查询。

 本文出处:http://blog.csdn.net/dba_huangzj/article/details/50202371

 

总结:


动态SQL很是强大。但是假设读者概括能力比較强的话,可以看到,动态SQL的问题主要是在不能很是好地利用计划缓存或使用的是不合适的运行计划,致使性能问题。

对于这类状况,有很是多方法可以使用。并且假设可以,最好仍是考虑非数据库层面的其它技术。

但是咱们的目的仍是一个:保证运行计划针对不论什么參数,最起码绝大部分參数都是最佳的。并且可以尽量重用。

最后。需要提醒的是。不论什么技术、技巧。都应该在尽量贴近实际环境的測试环境中作充分的測试,以便获得你但愿的结果。

 本文出处:http://blog.csdn.net/dba_huangzj/article/details/50202371

相关文章
相关标签/搜索