SQL Server 存儲過程的幾種常見寫法分析,我們該用那種寫法

来源:http://www.cnblogs.com/wy123/archive/2016/10/14/5958047.html
-Advertisement-
Play Games

本文出處: http://www.cnblogs.com/wy123/p/5958047.html 最近發現還有不少做開發的小伙伴,在寫存儲過程的時候,在參考已有的不同的寫法時,往往很迷茫,不知道各種寫法孰優孰劣,該選用那種寫法,以及各種寫法優缺點,本文以一個簡單的查詢存儲過程為例,簡單說一下各種寫 ...


 

本文出處: 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)
)
GO
declare @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,可能會抑制索引的情況

      為什麼說可能會抑制到索引的時候?上面提到過,SQL在執行之前是需要編譯的,
      因為在編譯的時候並不知道查詢條件是否傳入了值,有可能為null,有可能是一個具體的值
      SQL Server為了保險起見,採用了全表掃描的方式,舉個簡單的例子

      

      如果我直接帶入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的重點

 

總結:

  以上總結了三種在開發中比較常見的存儲過程的寫法,每種存儲過程的寫法可能在不同的公司都用應用,
  是不是有人挑個最簡單最快捷(第二種)寫法,寫完不是完事了,而是埋雷了。
  不是太熟悉SQL Server的同學可能會有點迷茫,有很多種寫法,究竟要用哪種寫法這些寫法之間有什麼區別。
  本文通過一個簡單的示例,說了常見的幾種寫法之間的區別,每種方式存在的問題,以及孰優孰劣,請小伙伴們明辨。
  資料庫大神請無視,謝謝。

 

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文出處:http://www.cnblogs.com/wy123/p/5960825.html 我們在做開發的時候,有時候會需要一些幫助數據,必須需要連續的數字,連續間隔的時間點,連續的季度日期等等常見很多人利用master庫的spt_values系統表,這個當然沒有問題 比如下麵這個(沒截完,結 ...
  • datediff語法格式:datediff(day,開始時間,結束時間) 一、應用舉例: 上面的代碼,將查詢 'created_time' > '2016-09-20' 的所有記錄。 如果要查詢'created_time' < '2016-09-20' 的所有記錄,有兩個方法: 1、把最後的>1,改 ...
  • 代碼: spt_values說明: 應用場景: 按月份的統計時,若某些月份無數據,統計結果就不會是月份上連續的。可以通過下麵方法得到月份連續的結果: ...
  • 工作小計:更換IP後監聽無法啟動解決。工作環境轉實驗環境記錄。 啟動監聽報錯 查看錯誤日誌host_addr有誤 分別再次檢查tnsnames.ora listener.ora和hosts文件,最終找到hosts文件IP錯誤 更改之後再次啟動監聽 ...
  • DML語句 insert 向表中插入新的記錄 --三種插入方式 --(1)不寫欄位的名字,直接按照欄位的順序把值逐個往裡插 insert into dept2 values(50,'DANAME','BEIJING'); --(2)指定某些欄位往裡插,其他不插的欄位預設都是空值 insert int... ...
  • RowVersion數據類型是系統自動生成的,唯一的,二進位數字,數值和binary(8)相同,RowVersion通常用作給Table的數據行加版本戳,存儲大小為 8 個位元組。RowVersion數據類型只是永恆遞增的數字,不保留日期或時間,但是可以使用RowVersion來比較數據行更新時間的先 ...
  • JDBC_day03 批處理 預編譯的批處理執行insert 語句 addBatch(); 非預編譯批處理 預編譯批處理 JDBC事務 所謂事務就是所有的sql操作要麼一起成功,要麼一起失敗,類似於銀行一個賬戶給另一個賬戶轉賬. 事務的特性 : 原子性,一致性,隔離性或獨立性,持久性 步驟: 1. ...
  • 參考 SQL技術內幕 寫了一段腳本,可以通過這段腳本查看執行指定SQL語句後,系統生成了哪些執行計劃.使用時註意以下幾點: 1. 修改 ,換成自己的資料庫名字. 2. 將 替換成要測試的SQL語句 3. 該腳本只支持Sql Server 2005及以上版本 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...