淺析Sql Server引數化查詢
錯誤認識1.不需要防止sql注入的地方無需引數化
引數化查詢就是為了防止SQL注入用的,其它還有什麼用途不知道、也不關心,原則上是能不用引數就不用引數,為啥?多麻煩,我只是做公司內部系統不用擔心SQL注入風險,使用引數化查詢不是給自己找麻煩,簡簡單單拼SQL,萬事OK
錯誤認識2.引數化查詢時是否指定引數型別、引數長度沒什麼區別
以前也一直都覺的加與不加引數長度應該沒有什麼區別,僅是寫法上的不同而已,而且覺得加引數型別和長度寫法太麻煩,最近才明白其實兩者不一樣的,為了提高sql執行速度,請為SqlParameter引數加上SqlDbType和size屬性,在引數化查詢程式碼編寫過程中很多開發者忽略了指定查詢引數的型別,這將導致託管程式碼在執行過程中不能自動識別引數型別,進而對該欄位內容進行全表掃描以確定引數型別並進行轉換,消耗了不必要的查詢效能所致。根據MSDN解釋:如果未在size引數中顯式設定Size,則從dbType引數的值推斷出該大小。如果你認為上面的推斷出該大小是指從SqlDbType型別推斷,那你就錯了,它實際上是從你傳過來的引數的值來推斷的,比如傳遞過來的值是"username",則size值為8,"username1",則size值為9。那麼,不同的size值會引發什麼樣的結果呢?且經測試發現,size的值不同時,會導致資料庫的執行計劃不會重用,這樣就會每次執行sql的時候重新生成新的執行計劃,而浪費資料庫執行時間。
下面來看具體測試
首先清空查詢計劃
DBCC FREEPROCCACHE
傳值username,不指定引數長度,生成查詢計劃
using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); SqlCommand comm = new SqlCommand(); comm.Connection = conn; comm.CommandText = "select * from Users where UserName=@UserName"; //傳值 username,不指定引數長度//查詢計劃為(@UserName varchar(8))select * from Users where UserName=@UserName comm.Parameters.Add(new SqlParameter("@UserName", SqlDbType.VarChar) { Value = "username" }); comm.ExecuteNonQuery(); }
傳值username1,不指定引數長度,生成查詢計劃
using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); SqlCommand comm = new SqlCommand(); comm.Connection = conn; comm.CommandText = "select * from Users where UserName=@UserName"; //傳值 username1,不指定引數長度 //查詢計劃為(@UserName varchar(9))select * from Users where UserName=@UserName comm.Parameters.Add(new SqlParameter("@UserName", SqlDbType.VarChar) { Value = "username1" }); comm.ExecuteNonQuery(); }
傳值username,指定引數長度為50,生成查詢計劃
using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); SqlCommand comm = new SqlCommand(); comm.Connection = conn; comm.CommandText = "select * from Users where UserName=@UserName"; //傳值 username,指定引數長度為50 //查詢計劃為(@UserName varchar(50))select * from Users where UserName=@UserName comm.Parameters.Add(new SqlParameter("@UserName", SqlDbType.VarChar,50) { Value = "username" }); comm.ExecuteNonQuery(); }
傳值username1,指定引數長度為50,生成查詢計劃
using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); SqlCommand comm = new SqlCommand(); comm.Connection = conn; comm.CommandText = "select * from Users where UserName=@UserName"; //傳值 username1,指定引數長度為50 //查詢計劃為(@UserName varchar(50))select * from Users where UserName=@UserName comm.Parameters.Add(new SqlParameter("@UserName", SqlDbType.VarChar,50) { Value = "username1" }); comm.ExecuteNonQuery(); }
使用下面語句檢視執行的查詢計劃
SELECT cacheobjtype,objtype,usecounts,sql FROM sys.syscacheobjects WHERE sql LIKE '%Users%' and sql not like '%syscacheobjects%'
結果如下圖所示
可以看到指定了引數長度的查詢可以複用查詢計劃,而不指定引數長度的查詢會根據具體傳值而改變查詢計劃,從而造成效能的損失。
這裡的指定引數長度僅指可變長資料型別,主要指varchar,nvarchar,char,nchar等,對於int,bigint,decimal,datetime等定長的值型別來說,無需指定(即便指定了也沒有用),詳見下面測試,UserID為int型別,無論長度指定為2、20、-1查詢計劃都完全一樣為(@UserIDint)select*from Users where UserID=@UserID
using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); SqlCommand comm = new SqlCommand(); comm.Connection = conn; comm.CommandText = "select * from Users where UserID=@UserID"; //傳值 2,引數長度2 //執行計劃(@UserID int)select * from Users where UserID=@UserID comm.Parameters.Add(new SqlParameter("@UserID", SqlDbType.Int, 2) { Value = 2 }); comm.ExecuteNonQuery(); } using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); SqlCommand comm = new SqlCommand(); comm.Connection = conn; comm.CommandText = "select * from Users where UserID=@UserID"; //傳值 2,引數長度20 //執行計劃(@UserID int)select * from Users where UserID=@UserID comm.Parameters.Add(new SqlParameter("@UserID", SqlDbType.Int, 20) { Value = 2 }); comm.ExecuteNonQuery(); } using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); SqlCommand comm = new SqlCommand(); comm.Connection = conn; comm.CommandText = "select * from Users where UserID=@UserID"; //傳值 2,引數長度-1 //執行計劃(@UserID int)select * from Users where UserID=@UserID comm.Parameters.Add(new SqlParameter("@UserID", SqlDbType.Int, -1) { Value = 2 }); comm.ExecuteNonQuery(); }
這裡提一下,若要傳值varchar(max)或nvarchar(max)型別怎麼傳,其實只要設定長度為-1即可
using (SqlConnection conn = new SqlConnection(connectionString)) { conn.Open(); SqlCommand comm = new SqlCommand(); comm.Connection = conn; comm.CommandText = "select * from Users where UserName=@UserName"; //型別為varchar(max)時,指定引數長度為-1 //查詢計劃為 (@UserName varchar(max) )select * from Users where UserName=@UserName comm.Parameters.Add(new SqlParameter("@UserName", SqlDbType.VarChar,-1) { Value = "username1" }); comm.ExecuteNonQuery(); }
當然了若是不使用引數化查詢,直接拼接SQL,那樣就更沒有查詢計劃複用一說了,除非你每次拼的SQL都完全一樣
總結,引數化查詢意義及注意點
1.可以防止SQL注入
2.可以提高查詢效能(主要是可以複用查詢計劃),這點在資料量較大時尤為重要
3.引數化查詢引數型別為可變長度時(varchar,nvarchar,char等)請指定引數型別及長度,若為值型別(int,bigint,decimal,datetime等)則僅指定引數型別即可
4.傳值為varchar(max)或者nvarchar(max)時,引數長度指定為-1即可
5.看到有些童鞋對於儲存過程是否要指定引數長度有些疑惑,這裡補充下,若呼叫的是儲存過程時,引數無需指定長度,如果指定了也會忽略,以儲存過程中定義的長度為準,不會因為沒有指定引數長度而導致重新編譯,不過還是建議大家即便時呼叫儲存過程時也加上長度,保持良好的變成習慣