在SQL SERVER的查詢語句中使用OR是否會導致不走索引查找(Index Seek)或索引失效(堆表走全表掃描 (Table Scan)、聚集索引表走聚集索引掃描(Clustered Index Scan))呢?是否所有情況都是如此?又該如何優化呢? 下面我們通過一些簡單的例子來分析理解這些現象。下面的實驗環境為SQL SERVER 2008,如果在不同版本有所區別,歡迎指正。
堆表單索引
首先我們構建我們測試需要實驗環境,具體情況如下所示:
DROP TABLE TEST
CREATE TABLE TEST (OBJECT_ID INT, NAME VARCHAR(32));
CREATE INDEX PK_TEST ON TEST(OBJECT_ID)
DECLARE @Index INT =0;
WHILE @Index < 500000
BEGIN
INSERT INTO TEST
SELECT @Index, 'kerry'+CAST(@Index AS VARCHAR(6));
SET @Index = @Index +1;
END
UPDATE STATISTICS TEST WITH FULLSCAN
場景1:如下所示,並不是所有的OR條件都會導致SQL走全表掃描。具體情況具體分析,不要套用教條。
SELECT * FROM TEST WHERE (OBJECT_ID =5 OR OBJECT_ID = 105)
場景2:加了條件1=1后,執行計划從索引查找(Index Seek)變為全表掃描(Table Scan),為什么會如此呢?個人理解為優化器將OR運算拆分為兩個子集處理,由於一些原因,1=1這個條件導致優化器認定需要全表掃描才能完成1=1條件子集的計算處理(為了理解這個,煞費苦心,鑒於理論薄弱,如有錯誤或不足,敬請指出)。所以優化器在權衡代價后生成的執行計划最終選擇了全表掃描(Table Scan)
SELECT * FROM TEST WHERE (1=1 OR OBJECT_ID =105);
場景3: 下面場景比較好理解,因為下面需要從500000條記錄中取出499700條記錄,而全表掃描(Table Scan)肯定是最優的選擇,代價(Cost)最低。
SELECT * FROM TEST WHERE (OBJECT_ID >300 OR OBJECT_ID =105);
場景4:這種場景跟場景2的情況本質是一樣的。所以在此略過。其實類似這種寫法也是實際情況中最常出現的情況,還在迷糊的同學,趕緊拋棄這種寫法吧
DECLARE @OBJECT_ID INT =150;
SELECT * FROM TEST WHERE (@OBJECT_ID IS NULL OR OBJECT_ID =@OBJECT_ID);
聚集索引表單索引
在聚集索引表中,我們也依葫蘆畫瓢,准備實驗測試的數據環境。
DROP TABLE TEST
CREATE TABLE TEST (OBJECT_ID INT, NAME VARCHAR(32));
CREATE CLUSTERED INDEX PK_TEST ON TEST(OBJECT_ID)
DECLARE @Index INT =0;
WHILE @Index < 500000
BEGIN
INSERT INTO TEST
SELECT @Index, 'kerry'+CAST(@Index AS VARCHAR(6));
SET @Index = @Index +1;
END
UPDATE STATISTICS TEST WITH FULLSCAN
場景1 :索引查找(Index Seek)
SELECT * FROM TEST WHERE (OBJECT_ID =5 OR OBJECT_ID = 105)
場景2:聚集索引掃描(Clustered Index Scan)
場景3:似乎與堆表有所不同。聚集索引表居然還是走聚集索引查找。
場景4:OR導致聚集索引掃描
如果堆表或聚集索引表上建立有聯合索引,情況也大致如此,在此不做過多案例講解。下面僅僅講述一兩個案例場景。
DROP TABLE test1;
CREATE TABLE test1
(
a INT,
b INT,
c INT,
d INT,
e INT
)
DECLARE @Index INT =0;
WHILE @Index < 10000
BEGIN
INSERT INTO test1
SELECT @Index,
@Index,
@Index,
@Index,
@Index
SET @Index = @Index + 1;
END
CREATE INDEX idx_test_n1
ON test1(a, b, c, d)
UPDATE STATISTICS test1 WITH fullscan;
SELECT * FROM TEST1 WHERE A=12 OR B> 500 OR C >100000
因為結果集是幾個條件的並集,最多只能在查找A=12的數據時用索引,其它幾個條件都需要表掃描,那優化器就會選擇直接走一遍表掃描,以最低的代價COST完成,所以索引就失效了。
那么如何優化查詢語句含有的OR的SQL語句呢?方法無外乎有三種:
1:通過索引覆蓋,使包含OR的SQL走索引查找(Index Seek)。但是這個只能滿足部分場景,並不能解決所有這類SQL。這個Solution具有一定的局限性。
SELECT * FROM TEST1 WHERE A=12 OR B=500
如果我們通過索引覆蓋,在字段B上面也建立索引,那么下面OR查詢也會走索引查找。
CREATE INDEX IDX_TEST1_B ON TEST1(B);
SELECT * FROM TEST1 WHERE A=12 OR B=500
2:使用IN替換OR。 但是這個Solution也有很多局限性。在此不做過多闡述。
3:一般將OR的字句分解成多個查詢,並且通過UNION ALL 或UNION連接起來。在聯合索引或有索引覆蓋的場景下。大部分情況下,UNION ALL的效率更高。但是並不是所有的UNION ALL都會比OR的SQL的代價(COST),特殊的情況或特殊的數據分布也會出現UNION ALL比OR代價要高的情況。例如,上面特殊的要求,從全表中取兩條記錄,如下所示
SELECT * FROM TEST1 WHERE A=12
UNION ALL
SELECT * FROM TEST1 WHERE B=500
UNON ALL語句的代價(Cost)要高與OR是因為它做了兩次索引查找(Index Seek),而OR語句只做一次索引查找(Index Seek)就完成了。開銷明顯小一些,但是實際情況這類特殊情況比較少,實際情況的取數條件、數據都比這個簡單案例要復雜得多。所以在大部分情況下,拆分為UNION ALL語句的效率要高於OR語句
另外一個案例,就是最上面實驗的堆表TEST, 在字段OBJECT_ID上建有索引
SELECT * FROM TEST WHERE (OBJECT_ID >300 OR OBJECT_ID =105);
SELECT * FROM TEST WHERE OBJECT_ID >300
UNION ALL
SELECT * FROM TEST WHERE OBJECT_ID =105;
可以從下面看出兩者開銷不同的地方在於IO方面,兩者開銷之所以有區別,是因為第二個SQL多了一次掃描(索引查找)
總結:
在實際開發環境中,OR這種寫法確實會帶來很多不確定性,盡量使用UNION 或IN替換OR。我們需要遵循一些規則,但是也不能認為它就是一成不變的,永為真理。具體場景、具體環境具體分析。要知其然知其所以然。在微軟亞太區數據庫技術支持組的官方博客中就有一個案例SQL Server性能問題案例解析 (3)也是OR引起的性能案例。 博客中有個觀點,我覺得挺贊的:”需要注意的是,對於OR或UNION,並沒有確定的孰優孰劣,使用時要進行測試才能確定。“ 。