sqlserver存储过程中处理多个查询条件的几种常见写法分析,我们该用那种写法(代码片段)

superfeeling superfeeling     2022-12-23     622

关键词:

本文出处: http://www.cnblogs.com/wy123/p/5958047.html 

 

最近发现还有不少做开发的小伙伴,在写存储过程的时候,在参考已有的不同的写法时,往往很迷茫,
不知道各种写法孰优孰劣,该选用那种写法,以及各种写法优缺点,本文以一个简单的查询存储过程为例,简单说一下各种写法的区别,以及该用那种写法
专业DBA以及熟悉数据库的同学请无视。

 

废话不多,上代码说明,先造一个测试表待用,简单说明一下这个表的情况

类似订单表,订单表有订单ID,客户ID,订单创建时间等,查询条件是常用的订单ID,客户ID,以及订单创建时间

create table SaleOrder
(
    id       int identity(1,1),
    OrderNumber  int         ,
    CustomerId   varchar(20)      ,
    OrderDate    datetime         ,
    Remark       varchar(200)
)GOdeclare @i int=0
while @i<100000
begin
    insert into SaleOrder values (@i,CONCAT(\'C\',cast(RAND()*1000 as int)),GETDATE()-RAND()*100,NEWID())
    set @i=@i+1
end
create index idx_OrderNumber on SaleOrder(OrderNumber)
create index idx_CustomerId on SaleOrder(CustomerId)
create index idx_OrderDate on SaleOrder(OrderDate)

生成的测试数据大概就是这个样子的

 

 下面演示说明几种常见的写法以及每种写法潜在的问题

第一种常见的写法:拼凑字符串,用EXEC的方式执行这个拼凑出来的字符串,不推荐

create proc pr_getOrederInfo_1
(
    @p_OrderNumber       int      ,
    @p_CustomerId        varchar(20) ,
    @p_OrderDateBegin    datetime   ,
    @p_OrderDateEnd      datetime
)
as
begin
    
    set nocount on;
    declare @strSql nvarchar(max);
    set @strSql= \'SELECT [id]
               ,[OrderNumber]
               ,[CustomerId]
               ,[OrderDate]
               ,[Remark]
            FROM [dbo].[SaleOrder] where 1=1 \';
    /*
        这种写法的特点在于将查询SQL拼凑成一个字符串,最后以EXEC的方式执行这个SQL字符串
    */

    if(@p_OrderNumber is not null)
        set @strSql = @strSql + \' and OrderNumber = \' + @p_OrderNumber
    if(@p_CustomerId is not null)
        set @strSql = @strSql + \' and CustomerId  = \'+ \'\'\'\'+ @p_CustomerId + \'\'\'\'
    if(@p_OrderDateBegin is not null)
        set @strSql = @strSql + \' and OrderDate >= \' + \'\'\'\' + cast(@p_OrderDateBegin as varchar(10)) + \'\'\'\'
    if(@p_OrderDateEnd is not null)
        set @strSql = @strSql + \' and OrderDate <= \' + \'\'\'\' + cast(@p_OrderDateEnd as varchar(10)) + \'\'\'\'

    print @strSql
    exec(@strSql);

end

假如我们查询CustomerId为88,在2016-10-1至2016-10-3这段时间内的订单信息,如下,带入参数执行

exec pr_getOrederInfo_1
@p_OrderNumber      = null      ,
@p_CustomerId       = \'C88\'     ,
@p_OrderDateBegin   = \'2016-10-1\' ,
@p_OrderDateEnd     = \'2016-10-3\'

首先说明,这种方式执行查询是完全没有问题的如下截图,结果也查出来了(当然结果也是没问题的)

我们把执行的SQL打印出来,执行的SQL语句本身就是就是存储过程中拼凑出来的字符串,这么一个查询SQL字符串

SELECT [id]
    ,[OrderNumber]
    ,[CustomerId]
    ,[OrderDate]
    ,[Remark]
FROM [dbo].[SaleOrder] 
where 1=1  
    and CustomerId  = \'C88\' 
    and OrderDate >= \'2016-10-1\' 
    and OrderDate <= \'2016-10-3\'

那么这种存储过程的有什么问题,或者直接一点说,这种方式有什么不好的地方

    其一,绕不过转移符(以及注入问题)

       在拼凑字符串时,把所有的参数都当成字符串处理,当查询条件本身包含特殊字符的时候,比如 \' 符号,
       或者其他需要转义的字符时,你拼凑的SQL就被打断了
       举个不恰当的例子,比如字符串中 @p_CustomerId中包含 \' 符号,直接就把你拼SQL的节凑给打乱了
       拼凑的SQL就变成了这个样子了,语法就不通过,更别提执行

SELECT [id]
              ,[OrderNumber]
              ,[CustomerId]
              ,[OrderDate]
              ,[Remark]
          FROM [dbo].[SaleOrder] 
          where 1=1  and CustomerId  = \'C\'88\'

一方面需要处理转移符,另一方面需要要防止SQL注入

   其二,参数不同就必须重新编译
        这种拼凑SQL的方式,如果每次查询的参数不同,拼凑出来的SQL字符串也不一样,
        如果熟悉SQL Server的同学一定知道,只要你执行的SQL文本不一样,
        比如
        第一次是执行查询 *** where CustomerId = \'C88\' ,
                   第二次是执行查询 *** where CustomerId = \'C99\' ,因为两次执行的SQL文本不同
        每次执行之前必然需要对其进行编译,编译的话就需要CPU,内存资源
        如果存在大批量的SQL编译,无疑要消耗更多的CPU资源(当然也需要一些内存资源)

 

 

第二种常见的写法:对所有查询条件用OR的方式加在where条件中,非常不推荐

create proc pr_getOrederInfo_2
(
    @p_OrderNumber      int      ,
    @p_CustomerId       varchar(20) ,
    @p_OrderDateBegin   datetime   ,
    @p_OrderDateEnd     datetime
)
as
begin
    
    set nocount on;

    declare @strSql nvarchar(max);

    SELECT [id]
            ,[OrderNumber]
            ,[CustomerId]
            ,[OrderDate]
            ,[Remark]
    FROM [dbo].[SaleOrder] 
    where 1=1
        and (@p_OrderNumber is null  or OrderNumber  = @p_OrderNumber)
        and (@p_CustomerId  is null  or CustomerId   = @p_CustomerId)
        /*
        这是另外一种类似的奇葩的写法,下面会重点关注
        and  OrderNumber  = ISNULL( @p_OrderNumber,OrderNumber)
        and  CustomerId   = ISNULL( @p_CustomerId,CustomerId)
        */
        and (@p_OrderDateBegin is null or OrderDate  >= @p_OrderDateBegin)
        and (@p_OrderDateEnd is null   or OrderDate  <= @p_OrderDateEnd)
        
end

首先看这种方式的执行结果,带入同样的参数,跟上面的结果一样,查询(结果)本身是没有任何问题的

  这种写法写起来避免了拼凑字符串的处理,看起来很简洁,写起来也很快,稀里哗啦一个存储过程就写好了,
  发布到生产环境之后就相当于埋了一颗雷,随时引爆。
  因为一条低效而又频繁执行的SQL,拖垮一台服务器也是司空见惯
  但是呢,问题非常多,也非常非常不推荐,甚至比第一种方式更糟糕。

  分析一下这种处理方式的逻辑:
  这种处理方式,因为不确定查询的时候到底有没有传入参数,也就数说不能确定某一个查询条件是否生效,
  于是就采用类似 and (@p_OrderNumber is null or OrderNumber = @p_OrderNumber)这种方式,来处理参数,
  这样的话
  如果@p_OrderNumber为null,or的前者(@p_OrderNumber is null)成立,后者不成立,查询条件不生效
  如果@p_OrderNumber为非null,or的后者(OrderNumber = @p_OrderNumber)成立而前者不成立,查询条件生效
  总之来说,不管参数是否为空,都可以有效地拼凑到查询条件中去。
  避免了拼SQL字符串,既做到让参数非空的时候生效,有做到参数为空的时候不生效,看起来不错,是真的吗?

  那么这种存储过程的有什么问题?

    1,会抑制索引的情况

      如图,带入参数值执行存储过程,先忽略另外三个查询字段,只传入@p_CustomerId参数,
      相关查询列上(CustomerId)有索引,但是这里走的是CustomerId列上的Index Scan而非预期的Index Seek

      

 

      为什么说可能会抑制到索引的时候?上面提到过,SQL在执行之前是需要编译的,
      因为在编译的时候并不知道查询条件是否传入了值,有可能为null,有可能是一个具体的值

       纠错:上面的一句话,使用参数做编译的时候,是知道参数的值的(只有使用本地变量的时候才不知道具体的参数值,直接使用参数确实是知道的),
          编译也是根据具体的参数值来生成执行计划的,但是为什么即使知道具体的参数值的情况下,依然生成一个Index Scan的方式,而不是期望的Index Seek?
          即便是存储过程在编译的时候知道了参数的值,为什么仍旧用不到索引?
          还要从and (@p_CustomerId  is null  or CustomerId   = @p_CustomerId)这种写法入手分析。

         即便是CustomerId列上有索引,
           如果@p_CustomerId  参数非空,走索引Seek完全没有问题。
                      如果@p_CustomerId  为null,此时and (@p_CustomerId  is null  or CustomerId   = @p_CustomerId)这个条件恒成立,如果再走索引Seek会出现什么结果?
                      语义上变成了是查找CustomerId  为null的值,如果采用Index Seek的方式执行,这样的话逻辑上已经错误了。
                    因此出现这种写法,为了安全起见,优化器只能选择一个索引的扫描(即便是字段上有索引的情况下)

         可以认为是这种写法在语义支持不了相关索引的Seek,而索引的Scan是处理这种写法的一种安全的方式

         The optimiser can tell that and it plays safe. It creates plans that will always work.
         That’s (one of the reasons) why in the first example it was an index scan, not an index seek.

        参考这里,可以简单地理解成这种写法,语义上支持不了索引的Seek,最多支持到index scan

      至于(@p_CustomerId  is null or CustomerId  = @p_CustomerId  )这种写法遇到本地变量的时候,
      为什么抑制到到索引的使用,我之前也是没有弄清楚的,评论中10楼Uest 给出了解释,这里非常感谢Uest

      如下

      

      如果我直接带入CustomerId=‘C88’,再来看执行计划,结果跟上面一样,但是执行计划是完全不一样的,这就是所谓的抑制到索引的使用。

      

   

   2,非常非常致命的逻辑错误

/*
这是另外一种类似的奇葩的写法,需要重点关注,真的就能满足“不管参数是否为空都满足”
            and  OrderNumber = ISNULL( @p_OrderNumber,OrderNumber)
            and  CustomerId  = ISNULL( @p_CustomerId,CustomerId)
*/

对于如下这种写法:OrderNumber = ISNULL( @p_OrderNumber,OrderNumber),
    一部分人非常推崇,认为这种方式简单、清晰,我也是醉了,有可能产生非常严重的逻辑错误
    如果参数为null,就转换成这种语义 where 1=1 and OrderNumber = OrderNumber
    目的是查询参数为null,查询条件不生效,让这个查询条件恒成立,恒成立吗,不一定,某些情况下就会有严重的语义错误 

    博主发现这个问题也是因为某些实际系统中的bug,折腾了好久才发现这个严重的逻辑错误 http://www.cnblogs.com/wy123/p/5580821.html

    对于这种写法,
    不管是第一点说的抑制索引的问题,数据量大的时候是非常严重的,上述写法会造成全表(索引)扫描,有索引也用不上,至于全表(索引)扫描的坏处就不说了
    还是第二点说的造成的逻辑错误,都是非常致命的
    所以这种方式是最最不推荐的。

 

 

第三种常见的写法:参数化SQL,推荐

create proc pr_getOrederInfo_3
(
    @p_OrderNumber       int      ,
    @p_CustomerId        varchar(20) ,
    @p_OrderDateBegin    datetime   ,
    @p_OrderDateEnd      datetime
)
as
begin
    
       set nocount on;
          DECLARE @Parm         NVARCHAR(MAX) = N\'\',
              @sqlcommand   NVARCHAR(MAX) = N\'\'

        SET @sqlcommand = \'SELECT [id]
                                  ,[OrderNumber]
                                  ,[CustomerId]
                                  ,[OrderDate]
                                  ,[Remark]
                            FROM [dbo].[SaleOrder] 
                            where 1=1 \'
        
        IF(@p_OrderNumber IS NOT NULL)
            SET @sqlcommand = CONCAT(@sqlcommand,\' AND OrderNumber= @p_OrderNumber\')

        IF(@p_CustomerId IS NOT NULL)
            SET @sqlcommand = CONCAT(@sqlcommand,\' AND CustomerId= @p_CustomerId\')

        IF(@p_OrderDateBegin IS NOT NULL)
            SET @sqlcommand = CONCAT(@sqlcommand,\' AND OrderDate>=@p_OrderDateBegin \')

        IF(@p_OrderDateEnd IS NOT NULL)
            SET @sqlcommand = CONCAT(@sqlcommand,\' AND OrderDate<=@p_OrderDateEnd \')

        SET @Parm= \'@p_OrderNumber        int,
                    @p_CustomerId        varchar(20),
                    @p_OrderDateBegin    datetime,
                    @p_OrderDateEnd        datetime \'
        PRINT @sqlcommand
        EXEC sp_executesql @sqlcommand,@Parm,
                            @p_OrderNumber       =    @p_OrderNumber,
                            @p_CustomerId        =    @p_CustomerId,
                            @p_OrderDateBegin    =    @p_OrderDateBegin,
                            @p_OrderDateEnd      =    @p_OrderDateEnd 
        
end

首先我们用同样的参数来执行一下查询,当然没问题,结果跟上面是一样的。

  

所谓的参数化SQL,就是用变量当做占位符,通过 EXEC sp_executesql执行的时候将参数传递进去SQL中,在需要填入数值或数据的地方,使用参数 (Parameter) 来给值,
这样的话,

第一,既能避免第一种写法中的SQL注入问题(包括转移符的处理),
   因为参数是运行时传递进去SQL的,而不是编译时传递进去的,传递的参数是什么就按照什么执行,参数本身不参与编译
第二,保证执行计划的重用,因为使用占位符来拼凑SQL的,SQL参数的值不同并导致最终执行的SQL文本不同
   同上面,参数本身不参与编译,如果查询条件一样(SQL语句就一样),而参数不一样,并不会影响要编译的SQL文本信息
第三,还有就是避免了第二种情况(and (@p_CustomerId is null or CustomerId = @p_CustomerId)
   或者 and OrderNumber = ISNULL( @p_OrderNumber,OrderNumber))
    这种写法,查询条件有就是有,没有就是没有,不会丢给SQL查询引擎一个模棱两个的结果,
    避免了对索引的抑制行为,是一种比较好的处理查询条件的方式。

缺点,1,对于这种方式,也有一点不好的地方,就是拼凑的字符串处理过程中,
      调试具体的SQL语句的时候,参数是直接拼凑在SQL文本中的,不能直接执行,要手动将占位参数替换成具体的参数值

      2,可能存在parameter sniff问题,但是对于parameter sniff问题,不是否定参数化SQL的重点,当然解决parameter sniff问题的办法还是有的,

       参考:http://www.cnblogs.com/wy123/p/5645485.html

 

总结:

  以上总结了三种在开发中比较常见的存储过程的写法,每种存储过程的写法可能在不同的公司都用应用,
  是不是有人挑个最简单最快捷(第二种)写法,写完不是完事了,而是埋雷了。
  不是太熟悉SQL Server的同学可能会有点迷茫,有很多种写法,究竟要用哪种写法这些写法之间有什么区别。
  本文通过一个简单的示例,说了常见的几种写法之间的区别,每种方式存在的问题,以及孰优孰劣,请小伙伴们明辨。
  数据库大神请无视,谢谢。

参数为空取全部数据的几种做法

...结果如下:下面通过一个订单信息表来看下该需求的实现过程。首先在报表设计器中制作一个网格式报表,如图:并在该报表的基础上增加两个参数用于数据过滤:area:用于接收参数对地区进行过滤amount:接收参数对订单金额... 查看详情

如何做sqlserver数据查询优化!

...拆分表,表分区 参考技术C影响查询效率的因素    SQLServer处理查询计划的过程是这样的:在做完查询语句的词法、语法检查之后,将语句提交给SQLServer的查询优化器,查询优化器通过检查索引的存在性、有效性和基于列的... 查看详情

MySQL存储过程,处理多个游标和查询结果

】MySQL存储过程,处理多个游标和查询结果【英文标题】:MySQLstoredprocedure,handlingmultiplecursorsandqueryresults【发布时间】:2010-01-0314:55:50【问题描述】:如何在同一个例程中使用两个游标?如果我删除第二个游标声明并获取循环一... 查看详情

用于从具有多个条件的多个表中的多个列中获取数据的存储过程

】用于从具有多个条件的多个表中的多个列中获取数据的存储过程【英文标题】:storedproceduretofetchdatafrommultiplecolumnsinmultipletableswithmultiplecriteria\'s【发布时间】:2017-10-1800:50:07【问题描述】:我在寻找什么?创建一个存储过程来... 查看详情

Apache DbUtils:处理从存储过程返回的多个结果集

...时间】:2016-08-0601:04:01【问题描述】:我在使用DbUtils从SQLServer中的存储过程中检索结果时遇到问题。在SQLServerManagementStudio中执行的存储过程在针对特定输入值执行时返 查看详情

是否可以在同一存储过程中并行处理for循环?

有一个SQLServer存储过程,我想循环遍历从表中读取的一组选项。所以说一张桌子有100个选项。我的存储过程将循环遍历这些选项,对于每个选项,我需要进行一些检查-通过查询基于选项的几个特定表并标记与其相关的状态。我... 查看详情

sqlserver创建视图添加where条件,条件包含一个参数

执行查询的时候,把该参数的值传进去进行查询视图是不可以传递参数的,如果要传参数可以考虑用存储过程等方法来解决。如,要做一个查询,其中表名是参数。创建存储过程create proc p_test(@tablename varchar(20))asdeclare @sql varc... 查看详情

将 SQL Server 存储过程转换为 Oracle 过程以从表中查询

】将SQLServer存储过程转换为Oracle过程以从表中查询【英文标题】:ConvertSQLServerstoredproceduretoOracleproceduretoqueryfromtables【发布时间】:2015-11-3022:55:09【问题描述】:我正在尝试从SQLServer迁移到Oracle数据库。我必须将我的存储过程从S... 查看详情

hibernate的条件查询的几种方式+查询所有的记录

条件查询1、第一种,用?占位符,如://登录(用?占位符)publicList<UserPO>LoginUser(UserPOup)throwsException{Sessionsession=HibernateSessionFactory.getSession();Stringhql="fromUserPOwherename=?andpwd=?";Queryquery=session 查看详情

三jpa复杂查询的几种方式(代码片段)

...,find后跟字段的属性名字,参数传入字段的属性类型,多个条件中间用and区分开。实例:接口:OrderfindByExpressNo(StringexpressNo);List<Order>findByUserId(IntegeruserId);List<Order>findByUserNameAndUserAddress(StringuserName,StringuserAddress);测试类... 查看详情

Amazon Red Shift:如何在 SQL Server 中编写类似于存储过程的查询批处理

】AmazonRedShift:如何在SQLServer中编写类似于存储过程的查询批处理【英文标题】:AmazonRedShift:HowtowritequerybatchessimilartoStoredProceduresinSQLServer【发布时间】:2013-09-0314:10:41【问题描述】:我们正在尝试将基于SQLServer的应用程序移植到... 查看详情

sqlserver中怎样创建保存数据的存储过程

在SQLServer中,可以使用两种方法创建存储过程:利用SQLServer管理平台创建存储过程。使用Transact-SQL语句中的CREATEPROCEDURE命令创建存储过程。创建存储过程时,需要确定存储过程的几个组成部分:①所有的输入参数以及传给调用者... 查看详情

约束的几种状态

  为了更好地处理数据可能暂时违反约束条件的情况,可将约束条件指定为不同的状态。可以启用(ENABLE)或禁用(DISABLE)完整性约束条件。如果启用约束条件,在数据库中输入或更新数据时就会检查数据。此时,禁止输入不... 查看详情

从同一个表mysql存储过程中选择具有不同条件的多个计数

】从同一个表mysql存储过程中选择具有不同条件的多个计数【英文标题】:Selectingmultiplecountwithdifferentconditionsfromsametablemysqlstoredprocedure【发布时间】:2019-10-0311:13:33【问题描述】:我有一张像下面这样的表格tbl_testtest_idnameval1ab12a... 查看详情

获取存储过程返回值的几种方式

1正常存储过程带RETURN(只能返回整型)CREATEPROCEDUREp_test1ASDECLARE@intintSET@int=102400;RETURN@int;--这里只能返回整型--执行DECLARE@p1returnINT--声明一个变量EXECUTE@p1return=p_test1--使用变量来接收return回来的值SELECT@p1return  &nb 查看详情

复合(组合)索引失效的几种情况总结(代码片段)

...定的多个列是有顺序的,某一个列没有出现在查询条件中,存储引擎不能使用索引中该列及其后的所有列。举例:为emp表插入索引idx_age_deptid 查看详情

变量名称在查询批处理或存储过程中必须是唯一的吗?

】变量名称在查询批处理或存储过程中必须是唯一的吗?【英文标题】:Variablenamesmustbeuniquewithinaquerybatchorstoredprocedure?【发布时间】:2021-12-1517:35:05【问题描述】:我们如何在一个事务中运行多个sql脚本,其中每个脚本可能定义... 查看详情

在 Oracle 存储过程中搜索文本

...NEXTVAL来获取下一个ID号的任何地方。我多年来一直在使用SQLServer,并且知道在那里做这件事的几种方法,但在这里没有一个对我 查看详情