一.概述
書寫sql是我們程序猿在開發中必不可少的技能,優秀的sql語句,執行起來吊炸天,性能杠杠的。差勁的sql,不僅使查詢效率降低,維護起來也十分不便。一切都是為了性能,一切都是為了業務,你覺得你的sql技能如何?所有的偉大來自於點滴的積累,不積跬步無以至千里,讓sql性能飛起來吧!

二.sql初探
1.常見sql寫法注意點
(1)字符類型建議采用varchar/nvarchar數據類型
- char
char是定長的,也就是當你輸入的字符小於你指定的數目時,char(8),你輸入的字符小於8時,它會再后面補空值。當你輸入的字符大於指定的數時,它會截取超出的字符。
nvarchar(n)
包含 n 個字符的可變長度 Unicode 字符數據。n 的值必須介於 1 與 4,000 之間。字節的存儲大小是所輸入字符個數的兩倍。所輸入的數據字符長度可以為零。
varchar[(n)]
長度為 n 個字節的可變長度且非 Unicode 的字符數據。n 必須是一個介於 1 和 8,000 之間的數值。存儲大小為輸入數據的字節的實際長度,而不是 n 個字節。所輸入的數據字符長度可以為零。
[1]—CHAR。CHAR存儲定長數據很方便,CHAR字段上的索引效率級高,比如定義char(10),那么不論你存儲的數據是否達到了10個字節,都要占去10個字節的空間。
[2]—VARCHAR。存儲變長數據,但存儲效率沒有CHAR高。如果一個字段可能的值是不固定長度的,我們只知道它不可能超過10個字符,把它定義為 VARCHAR(10)是最合算的。VARCHAR類型的實際長度是它的值的實際長度+1。為什么“+1”呢?這一個字節用於保存實際使用了多大的長度。 從空間上考慮,用varchar合適;從效率上考慮,用char合適,關鍵是根據實際情況找到權衡點。
[3]—TEXT。text存儲可變長度的非Unicode數據,最大長度為2^31-1(2,147,483,647)個字符。
[4]—NCHAR、NVARCHAR、NTEXT。這三種從名字上看比前面三種多了個“N”。它表示存儲的是Unicode數據類型的字符。我們知道字符中,英文字符只需要一個字節存儲就足夠了,但漢字眾多,需要兩個字節存儲,英文與漢字同時存在時容易造成混亂,Unicode字符集就是為了解決字符集這種不兼容的問題而產生的,它所有的字符都用兩個字節表示,即英文字符也是用兩個字節表示。nchar、nvarchar的長度是在1到4000之間。和char、varchar比較起來,nchar、nvarchar則最多存儲4000個字符,不論是英文還是漢字;而char、varchar最多能存儲8000個英文,4000個漢字。可以看出使用nchar、nvarchar數據類型時不用擔心輸入的字符是英文還是漢字,較為方便,但在存儲英文時數量上有些損失。
所以一般來說,如果含有中文字符,用nchar/nvarchar,如果純英文和數字,用char/varchar。
舉例說明:
兩字段分別有字段值:我和coffee
那么varchar字段占2×2+6=10個字節的存儲空間,而nvarchar字段占8×2=16個字節的存儲空間。
如字段值只是英文可選擇varchar,而字段值存在較多的雙字節(中文、韓文等)字符時用nvarchar
(2)金額貨幣建議采用money數據類型 (一般常用,最大四位小數)
(3)科學計數建議采用numeric數據類型-- (建議巨額資金交易用numeric)
(4)自增長標識建議采用bigint數據類型 (數據量一大,用int類型就裝不下,那以后改造就麻煩了)
(5)時間類型建議采用為datetime數據類型
(6)禁止使用text、ntext、image老的數據類型(已過時)
(7)禁止使用xml數據類型、varchar(max)、nvarchar(max)
(8)禁止在數據庫做復雜運算 (業務處理邏輯最好在代碼層實現,不要讓所有的代碼邏輯存在於sql中,不便於后期的問題定位)
(9)禁止使用SELECT * (按需所取,查找自己所需要的列)
(10)禁止在索引列上使用函數或計算
例如:
我們查詢注冊時間在2015-11-11的店鋪賬號,找出它們進行活動獎勵,我們如果不加注意,很可能寫成這樣:
select * from T_Account
where Convert(varchar(10,Regtime,121)='2015-11-11'
這樣寫的話,我們就無法命中索引字段Regtime,如果T_Account的數據量超大的時候,數據庫查詢分析器走表掃描,查詢效率就降低了;要實現上面的查詢結果,其實我們可以換一種寫法:
select * from T_Account
where Regtime>='2015-11-11 00:00:00'
and Regtime<'2015-11-12 00:00:00'
(11)禁止使用游標
由於游標在處理大數據量的時候,占有的內存較大,效率低。可能造成其他的數據庫查詢堵塞的現象,除非是當你使用while循環,子查詢,臨時表,表變量,自建函數或其他方式都無法處理某種操作的時候,再考慮使用游標。
舉例說明一下在實際運用中的一個游標處理:
--定義店鋪ID
declare @accId int
set @accId=218424
--1.創建臨時表並插入數據
select gsid,gid into #gidlist from T_Goods_Sku where accid=@accId and gid in (select gid from T_GoodsInfo where accid=@accId and isService=0 and IsExtend=1)
select gsId,gaVName into #gsidlist from T_Goods_Relation where gsid in (select gsid from T_Goods_Sku where accid=@accId and gid in (select gid from T_GoodsInfo where accid=@accId and isService=0 and IsExtend=1))
order by gsId
select a.gid gid,a.gsId gsId,b.gaVName gaVName into #tempgid from #gidlist a left join #gsidlist b
on a.gsId=b.gsId
drop table #gidlist
drop table #gsidlist
--2.開始事務
BEGIN TRANSACTION
--3.定義變量,累積事務執行過程中的錯誤
DECLARE @error INT
SET @error = 0
--4.聲明游標
DECLARE goodsCursor CURSOR SCROLL
FOR
SELECT gid
,gsId
,gaVName
FROM #tempgid
--5.打開游標
OPEN goodsCursor
--6.聲明游標提取數據所要存放的變量
DECLARE @gid INT
,@gsId INT
,@gaVName NVARCHAR(400)
,@gUnionKey NVARCHAR(400)
--7.定位游標到哪一行
FETCH First
FROM goodsCursor
INTO @gid
,@gsId
,@gaVName
--8.提取成功,對數據操作,進行下一條數據的提取操作
WHILE @@fetch_status = 0
BEGIN
SET @gUnionKey = ''
SELECT @gUnionKey = gUnionKey from T_GoodsInfo where accid=@accId and isService=0 and IsExtend=1 and gid=@gid
SELECT @gUnionKey=@gUnionKey+'|'+@gaVName
PRINT '-----start-------'
PRINT @gid
PRINT @gsId
PRINT @gaVName
PRINT @gUnionKey
--更新gUnionKey
update T_GoodsInfo
set gUnionKey=@gUnionKey
where accid=@accId and isService=0 and IsExtend=1 and gid=@gid
PRINT '-----end--------'
--移動游標
FETCH NEXT
FROM goodsCursor
INTO @gid
,@gsId
,@gaVName
END
--9.判讀事務錯誤數,提交或回滾事務
IF @error <> 0 --有誤
BEGIN
PRINT '回滾事務'
ROLLBACK TRANSACTION
END
ELSE
BEGIN
PRINT '提交事務'
COMMIT TRANSACTION
END
--10.關閉並刪除游標,刪除臨時表
CLOSE goodsCursor
DEALLOCATE goodsCursor
drop table #tempgid
(12)禁止使用觸發器
觸發器在開發角度來講,不知道具體什么時候執行,對於業務來講不跟代碼邏輯一樣是顯示的呈現,所以導致后期的維護比較困難,所以要處理觸發器完成的服務,最好通過服務或者中間件去完成。
例如:
在微信收單的過程中,我們銷售結賬完成以后,需要通過短信向用戶手機推送消費消息,這時候用觸發器可能就是在結賬以后,觸發sql觸發器,寫入一條消息記錄到短信表記錄,走消息隊列,將短信發送出去。
反之,我們采用中間件,就可以將結賬以后的記錄,發送給消息中間件EasyNetQ,中間件將記錄異步寫入記錄,這樣有問題的話,只用確認中間件消息接受和發送的問題。
(13)禁止在查詢里指定索引
在sql里面指定索引索引是這樣定義的:
SELECT 字段名表
FROM 表名表
WITH (INDEX(索引名))
WHERE 查詢條件
如果在搜索的時候,指定了索引搜索,就會導致新建的索引無法生效,假如刪除了指定的索引,會導致程序崩潰,所以建議不采用指定索引進行搜索。
(14)變量/參數/關聯字段類型必須與字段類型一致
所謂的變量、參數、關聯字段類型一致指的是,數據庫中是什么類型,那么我們在成程序中傳入參數的過程中,建議保持一直,避免在查詢的時候,進行類型轉換,在大批量數據處理過程中,可能影響性能。
圖1類型:(程序中類型)
圖二類型:(數據中類型)

圖1、圖2中字段類型保持一致。
(15)參數化查詢
所謂的“參數化SQL”就是在應用程序設置SqlCommand.CommandText的時候使用參數(如:param1),然后通過SqlCommand.Parameters.Add來設置這些參數的值。這種做法會把你准備好的命令通過sp_executesql系統存儲過程來執行,使用參數化,最直接的好處就是防止SQL注入。也就是說使用這種方法,主要是為了保證數據庫的安全。禁止拼接sql語句。
另外參數化查詢有利於數據庫查詢計划的復用,比如我們查詢注冊日期大於2015-12-12和注冊日期大於2016-12-12不同的店鋪記錄,我們可能這樣寫:
select * from T_Account where Regtime>'2015-12-12'
select * from T_Account where Regtime>'2016-12-12'
上面兩條語句,可以完成我們上面的查詢結果集,但是sql查詢計划會進行兩次分析,導致查詢計划不能夠復用,如果用參數化查詢,則可以復用查詢計划:
declare @Regtime datetime;
set @Regtime='2015-12-12';
select * from T_Account where Regtime>@Regtime
set @Regtime='2016-12-12';
select * from T_Account where Regtime>@Regtime
只需要改變參數的值就可以了。
(16)限制JOIN個數
join表的次數不要過多,寫代碼的人,看到過多的join表記錄都會懵逼,何況數據庫了?會導致數據庫執行錯誤的執行計划,影響性能。
(17)關閉影響的行計數信息返回
在sql語句中,可以設置Set NoAccount on,關閉查詢受影響的行數,從而減少流量。
(18)除非必要SELECT語句都必須加上NOLOCK
這個是我們經常在開發中忽略的,加上nolock以后,在查詢的時候,不鎖表。不要只要自己爽,別人也要查詢數據的,占這茅坑不拉shi是不好哦。這也是我們內部工程師的必修課提高的。
(19)使用UNION ALL替換UNION
使用union 的時候,必須滿足兩個表具體相同數目的列。
union all 包含全部的記錄,union 包含去除重復后的結果集
Employees_China:
| E_ID| E_Name|
| :-------- | --------😐
| 01| Zhang, Hua|
| 02| Wang, Wei|
| 03| Carter, Thomas|
| 04| Yang, Ming|
Employees_USA:| E_ID| E_Name|
| :-------- | --------😐
| 01| Adams, John|
| 02| Bush, George|
| 03|Carter, Thomas|
| 04|Gates,Bill|
使用 UNION 命令
列出所有在中國和美國的不同的雇員名:
SELECT E_Name FROM Employees_China
UNION
SELECT E_Name FROM Employees_USA
結果集:
|| E_Name|
| :-------- |
| Zhang, Hua|
| Wang, Wei|
| Carter, Thomas|
| Yang, Ming|
|Adams, John|
| Bush, George|
|Gates, Bill|
使用 UNION ALL 命令
列出在中國和美國的所有的雇員:
SELECT E_Name FROM Employees_China
UNION ALL
SELECT E_Name FROM Employees_USA
結果集:
|| E_Name|
| :-------- |
| Zhang, Hua|
| Wang, Wei|
| Carter, Thomas|
| Yang, Ming|
|Adams, John|
| Bush, George|
|Carter, Thomas|
|Gates, Bill|
(20)查詢大量數據使用分頁或TOP
通過分頁批量獲取數據,避免全表掃描。
在.Net中,我們可以這樣寫來分頁獲取數據,通過分頁獲取圖片數據,進行地址替換操作。
/// <summary>
/// 批量替換圖片地址
/// </summary>
/// <param name="index"></param>
/// <param name="size"></param>
public static void BatchReplaceImgAddress(int index, int size)
{
const string strSql =
"select Id,AccId,ImgAddress from (select ROW_NUMBER() OVER ( ORDER BY id )
as rownumber,id as Id,accid as AccId,ge_Details as ImgAddress " +
"from t_GoodsExtend (nolock) ) as T where rownumber
BETWEEN (@index-1)*@size+1 AND @size*@index";
var imgAddressesItems =
DapperHelper.Query<ImgAddressModel>(strSql, new
{
index = index,
size = size
}).ToList();
if (!imgAddressesItems.Any())
{
return;
}
try
{
Console.WriteLine("正在處理{0}~{1}條數據:", (index - 1)*size + 1, ((index - 1)*size) + size);
foreach (var item in imgAddressesItems)
{
var imgItem = item;
if (string.IsNullOrWhiteSpace(imgItem.ImgAddress)) continue;
var imgAddress = imgItem.ImgAddress;
const string targetReplaceStr = "baidu.com/umupload";
const string targetNewStr = "baidu.com/mobileweb/detail2";
if (imgAddress.Contains(targetReplaceStr))
{
var newImgAddress = imgAddress.Replace(targetReplaceStr, targetNewStr);
const string updateImgStrSql = "update t_GoodsExtend
set ge_Details = @ge_Details where id= @id";
var updateResult = DapperHelper.Execute(updateImgStrSql, new
{
id = imgItem.Id,
ge_Details = newImgAddress,
});
if (updateResult > 0)
{
var message = string.Format("當前的店鋪Id為:{0},處理記錄的Id為:{1}", imgItem.AccId,imgItem.Id);
Console.WriteLine(message);
SimpleLog.Instance.WriteLogForFile("批量替換圖片地址日志", message);
}
}
}
}
catch (Exception ex)
{
SimpleLog.Instance.WriteLogForFile("批量替換圖片地址異常", ex);
}
BatchReplaceImgAddress(index + 1, size);
}
(21)NOT EXISTS替代NOT IN
1、in和exists
in是把外表和內表作hash連接,而exists是對外表作loop循環,每次loop循環再對內表進行查詢,一直以來認為exists比in效率高的說法是不准確的。如果查詢的兩個表大小相當,那么用in和exists差別不大;如果兩個表中一個較小一個較大,則子查詢表大的用exists,子查詢表小的用in;
例如:表A(小表),表B(大表)
select * from A where cc in(select cc from B) -->效率低,用到了A表上cc列的索引;
select * from A where exists(select cc from B where cc=A.cc) -->效率高,用到了B表上cc列的索引。
相反的:
select * from B where cc in(select cc from A) -->效率高,用到了B表上cc列的索引
select * from B where exists(select cc from A where cc=B.cc) -->效率低,用到了A表上cc列的索引。
2、not in 和not exists
not in 邏輯上不完全等同於not exists,如果你誤用了not in,小心你的程序存在致命的BUG,請看下面的例子:
create table #t1(c1 int,c2 int);
create table #t2(c1 int,c2 int);
insert into #t1 values(1,2);
insert into #t1 values(1,3);
insert into #t2 values(1,2);
insert into #t2 values(1,null);
select * from #t1 where c2 not in(select c2 from #t2); -->執行結果:無
select * from #t1 where not exists(select c2 from #t2 where #t2.c2=#t1.c2) -->執行結果:1 3
正如所看到的,not in出現了不期望的結果集,存在邏輯錯誤。如果看一下上述兩個select 語句的執行計划,也會不同,后者使用了hash_aj,所以,請盡量不要使用not in(它會調用子查詢),而盡量使用not exists(它會調用關聯子查詢)。如果子查詢中返回的任意一條記錄含有空值,則查詢將不返回任何記錄。如果子查詢字段有非空限制,這時可以使用not in,並且可以通過提示讓它用hasg_aj或merge_aj連接。
如果查詢語句使用了not in,那么對內外表都進行全表掃描,沒有用到索引;而not exists的子查詢依然能用到表上的索引。所以無論哪個表大,用not exists都比not in 要快。
3、in 與 = 的區別
select name from student where name in('zhang','wang','zhao');
與
select name from student where name='zhang' or name='wang' or name='zhao'
的結果是相同的。
(22)盡量避免使用OR運算符
舉例說明我們在查找當當前是行業版和高級版店鋪的賬號時,我們可能會這樣寫:
select id from T_Account where id in(
select accountId from T_Bussiness where aotjob=3 or aotjob=5
)
where后面使用了aotjob=3 or aotjob=5,這樣會導致數據庫查詢無法命中索引,會走全表掃描。所以在這里我們使用in則會比較好:
select id from T_Account where id in(
select accountId from T_Bussiness where aotjob in (3,5)
)
(23)like的查詢的索引
1.[Col1] like "abc%" --index seek 這個就用到了索引查詢
2.[Col1] like "%abc%" --index scan 而這個就並未用到索引查詢
3.[Col1] like "%abc" --index scan 這個也並未用到索引查詢
我想從上而三個例子中,大家應該明白,最好不要在LIKE條件前面用模糊匹配,否則就用不到索引查詢。
2.合理使用NULL屬性
新加的表,所有字段禁止NULL
(新表為什么不允許NULL?
允許NULL值,會增加應用程序的復雜性。你必須得增加特定的邏輯代碼,以防止出現各種意外的bug
三值邏輯,所有等號(“=”)的查詢都必須增加isnull的判斷。
Null=Null、Null!=Null、not(Null=Null)、not(Null!=Null) 都為unknown,不為true
舉例來說明一下:
如果表里面的數據如圖所示:

你想來找查找除了name等於aa的所有數據,然后你就不經意間用了
SELECT * FROM USERS WHERE NAME<>’aa’
結果發現與預期不一樣,事實上它只查出了name=bb而沒有查找出name=NULL的數據記錄
那我們如何查找除了name等於aa的所有數據,只能用ISNULL函數了
SELECT * FROM USERS WHERE ISNULL(NAME,1)<>’aa’
但是大家可能不知道ISNULL會引起很嚴重的性能瓶頸 ,所以很多時候最好是在應用層面限制用戶的輸入,確保用戶輸入有效的數據再進行查詢。
舊表新加字段,需要允許為NULL(避免全表數據更新 ,長期持鎖導致阻塞)(這個主要是考慮之前表的改造問題)
3.理解執行計划
所謂的執行計划,就是數據庫根據sql語句生成的一個執行順序。先執行什么,再執行什么。類似於我們的工作計划,先做什么,后做什么,從而使我們的效率達到最高。所以合理的執行計划,會讓數據庫干正確的事,提高效率。
在我們使用sql查詢的時候,通常是根據sql內部的查詢計划來進行的,也就是說不同的sql語句生成的查詢計划不同,所以要優化sql,我們寫出的sql要讓數據庫能夠生成正確執行計划,才能提高性能;反之寫出的sql語句,不容易被數據庫翻譯成合理的執行計划,就容易導致性能瓶頸。
例如:
select id from T_Account
select id From T_Account
這兩句查詢語句我們可以看出只是from關鍵字大小的區別,但是查詢分析器會認為是不同的語句,進行兩次解析。所以針對同一個查詢語句,在不同的地方我們應該保持一致,大小寫一致,查找字段一致。在數據庫中針對查詢,數據庫會緩存查詢計划,如果查詢的時候,存在已經解析的查詢計划,就會按照存在的查詢計划走,這樣就節省了解析生成查詢計划的時間,提高了查詢性能。
三.總結
關於查詢計划,准備細致的學習一下,明白不同查詢計划具體的含義。從而可以進行對應的優化。上面講到不對的地方,希望大家指出,一起學習,一起進步!


