搞懂這些SQL優化技巧,面試橫着走

點擊上方藍色字關注咱們~













BATJTMD 等大廠的面試難度愈來愈高,但不管從大廠仍是到小公司,一直不變的重點就是對 SQL 優化經驗的考察。一提到數據庫,面試官就會問「說一說你對 SQL 優化的看法吧?」。前端


圖片來自 Pexelsjava


SQL 優化已經成爲衡量程序猿優秀與否的硬性指標,甚至在各大廠招聘崗位職能上都有明碼標註,若是是你,在這個問題上能吊打面試官仍是會被吊打呢?python


有朋友疑問到,SQL 優化真的有這麼重要麼?以下圖所示,SQL 優化在提高系統性能中是:成本最低和優化效果最明顯的途徑。web


若是你的團隊在 SQL 優化這方面搞得很優秀,對大家整個大型系統可用性方面無疑是一個質的跨越,真的能讓大家老闆省下不止幾沓子錢。

優化成本:硬件>系統配置>數據庫表結構>SQL 及索引。面試


優化效果:硬件<系統配置<數據庫表結構<SQL 及索引。數據庫


String result = "嗯,不錯,";

if ("SQL優化經驗足") {
    if ("熟悉事務鎖") {
        if ("併發場景處理666") {
            if ("會打王者榮耀") {
                result += "明天入職" 
            }
        }
    }
else {
    result += "先回去等消息吧";


Logger.info("面試官:" + result );


別看了,上面這是一道送命題。


好了咱們言歸正傳,首先,對於MySQL層優化我通常聽從五個原則:
  • 減小數據訪問:設置合理的字段類型,啓用壓縮,經過索引訪問等減小磁盤 IO。小程序

  • 返回更少的數據:只返回須要的字段和數據分頁處理,減小磁盤 IO 及網絡 IO。緩存

  • 減小交互次數:批量 DML 操做,函數存儲等減小數據鏈接次數。服務器

  • 減小服務器 CPU 開銷:儘可能減小數據庫排序操做以及全表查詢,減小 CPU 內存佔用。微信

  • 利用更多資源:使用表分區,能夠增長並行操做,更大限度利用 CPU 資源。


總結到 SQL 優化中,就以下三點:
  • 最大化利用索引。

  • 儘量避免全表掃描。

  • 減小無效數據的查詢。


理解 SQL 優化原理 ,首先要搞清楚 SQL 執行順序。


SELECT 語句,語法順序以下:

1. SELECT 
2. DISTINCT <select_list>
3. FROM <left_table>
4. <join_type> JOIN <right_table>
5. ON <join_condition>
6. WHERE <where_condition>
7. GROUP BY <group_by_list>
8. HAVING <having_condition>
9. ORDER BY <order_by_condition>
10.LIMIT <limit_number>


SELECT 語句,執行順序以下:

FROM
<表名> # 選取表,將多個表數據經過笛卡爾積變成一個表。
ON
<篩選條件> # 對笛卡爾積的虛表進行篩選
JOIN <join, left join, right join...> 
<join表> # 指定join,用於添加數據到on以後的虛表中,例如left join會將左表的剩餘數據添加到虛表中
WHERE
<where條件> # 對上述虛表進行篩選
GROUP BY
<分組條件> # 分組
<SUM()等聚合函數> # 用於having子句進行判斷,在書寫上這類聚合函數是寫在having判斷裏面的
HAVING
<分組篩選> # 對分組後的結果進行聚合篩選
SELECT
<返回數據列表> # 返回的單列必須在group by子句中,聚合函數除外
DISTINCT
# 數據除重
ORDER BY
<排序條件> # 排序
LIMIT
<行數限制>


如下 SQL 優化策略適用於數據量較大的場景下,若是數據量較小,不必以此爲準,以避免多此一舉。


避免不走索引的場景


①儘可能避免在字段開頭模糊查詢,會致使數據庫引擎放棄索引進行全表掃描

以下:
SELECT * FROM t WHERE username LIKE '%陳%'

優化方式: 儘可能在字段後面使用模糊查詢。


以下:
SELECT * FROM t WHERE username LIKE '陳%'


若是需求是要在前面使用模糊查詢:
  • 使用 MySQL 內置函數 INSTR(str,substr)來匹配,做用相似於 Java 中的 indexOf(),查詢字符串出現的角標位置。

  • 使用 FullText 全文索引,用 match against 檢索。

  • 數據量較大的狀況,建議引用 ElasticSearch、Solr,億級數據量檢索速度秒級。

  • 當表數據量較少(幾千條兒那種),別整花裏胡哨的,直接用 like '%xx%'。


②儘可能避免使用 in 和 not in,會致使引擎走全表掃描

以下:

SELECT * FROM t WHERE id IN (2,3)


優化方式: 若是是連續數值,能夠用 between 代替。

以下:

SELECT * FROM t WHERE id BETWEEN 2 AND 3


若是是子查詢,能夠用 exists 代替。


以下:

-- 不走索引
select * from A where A.id in (select id from B);
-- 走索引
select * from A where exists (select * from B where B.id = A.id);


③儘可能避免使用 or,會致使數據庫引擎放棄索引進行全表掃描


以下:

SELECT * FROM t WHERE id = 1 OR id = 3


優化方式: 能夠用 union 代替 or。

以下:

SELECT * FROM t WHERE id = 1
   UNION
SELECT * FROM t WHERE id = 3


④儘可能避免進行 null 值的判斷,會致使數據庫引擎放棄索引進行全表掃描


以下:

SELECT * FROM t WHERE score IS NULL


優化方式: 能夠給字段添加默認值 0,對 0 值進行判斷。

以下:

SELECT * FROM t WHERE score = 0


⑤儘可能避免在 where 條件中等號的左側進行表達式、函數操做,會致使數據庫引擎放棄索引進行全表掃描


能夠將表達式、函數操做移動到等號右側,以下:
-- 全表掃描
SELECT * FROM T WHERE score/10 = 9
-- 走索引
SELECT * FROM T WHERE score = 10*9

⑥當數據量大時,避免使用 where 1=1 的條件


一般爲了方便拼裝查詢條件,咱們會默認使用該條件,數據庫引擎會放棄索引進行全表掃描。


以下:

SELECT username, age, sex FROM T WHERE 1=1


優化方式: 用代碼拼裝 SQL 時進行判斷,沒 where 條件就去掉 where,有 where 條件就加 and。

⑦查詢條件不能用 <> 或者 !=


使用索引列做爲條件進行查詢時,須要避免使用<>或者!=等判斷條件。


如確實業務須要,使用到不等於符號,須要在從新評估索引創建,避免在此字段上創建索引,改由查詢條件中其餘索引字段代替。


⑧where 條件僅包含複合索引非前置列

以下:複合(聯合)索引包含 key_part1,key_part2,key_part3 三列,但 SQL 語句沒有包含索引前置列"key_part1",按照 MySQL 聯合索引的最左匹配原則,不會走聯合索引。

select col1 from table where key_part2=1 and key_part3=2

⑨隱式類型轉換形成不使用索引

以下 SQL 語句因爲索引對列類型爲 varchar,但給定的值爲數值,涉及隱式類型轉換,形成不能正確走索引。

select col1 from table where col_varchar=123

⑩order by 條件要與 where 中條件一致,不然 order by 不會利用索引進行排序

以下:

-- 不走age索引
SELECT * FROM t order by age;

-- 走age索引
SELECT * FROM t where age > 0 order by age;


對於上面的語句,數據庫的處理順序是:
  • 第一步:根據 where 條件和統計信息生成執行計劃,獲得數據。

  • 第二步:將獲得的數據排序。當執行處理數據(order by)時,數據庫會先查看第一步的執行計劃,看 order by 的字段是否在執行計劃中利用了索引。若是是,則能夠利用索引順序而直接取得已經排好序的數據。若是不是,則從新進行排序操做。

  • 第三步:返回排序後的數據。


當 order by 中的字段出如今 where 條件中時,纔會利用索引而再也不二次排序,更準確的說,order by 中的字段在執行計劃中利用了索引時,不用排序操做。


這個結論不只對 order by 有效,對其餘須要排序的操做也有效。好比 group by 、union 、distinct 等。


⑪正確使用 hint 優化語句

MySQL 中可使用 hint 指定優化器在執行時選擇或忽略特定的索引。


通常而言,處於版本變動帶來的表結構索引變化,更建議避免使用 hint,而是經過 Analyze table 多收集統計信息。


但在特定場合下,指定 hint 能夠排除其餘索引干擾而指定更優的執行計劃:
  • USE INDEX 在你查詢語句中表名的後面,添加 USE INDEX 來提供但願 MySQL 去參考的索引列表,就可讓 MySQL 再也不考慮其餘可用的索引。

    例子: SELECT col1 FROM table USE INDEX (mod_time, name)...

  • IGNORE INDEX 若是隻是單純的想讓 MySQL 忽略一個或者多個索引,可使用 IGNORE INDEX 做爲 Hint。

    例子: SELECT col1 FROM table IGNORE INDEX (priority) ...

  • FORCE INDEX 爲強制 MySQL 使用一個特定的索引,可在查詢中使用FORCE INDEX 做爲 Hint。

    例子: SELECT col1 FROM table FORCE INDEX (mod_time) ...


在查詢的時候,數據庫系統會自動分析查詢語句,並選擇一個最合適的索引。可是不少時候,數據庫系統的查詢優化器並不必定老是能使用最優索引。


若是咱們知道如何選擇索引,可使用 FORCE INDEX 強制查詢使用指定的索引。

例如:

SELECT * FROM students FORCE INDEX (idx_class_id) WHERE class_id = 1 ORDER BY id DESC;


SELECT 語句其餘優化


①避免出現 select *


首先,select * 操做在任何類型數據庫中都不是一個好的 SQL 編寫習慣。


使用 select * 取出所有列,會讓優化器沒法完成索引覆蓋掃描這類優化,會影響優化器對執行計劃的選擇,也會增長網絡帶寬消耗,更會帶來額外的 I/O,內存和 CPU 消耗。


建議提出業務實際須要的列數,將指定列名以取代 select *。具體詳情見《爲何你們都說SELECT * 效率低

②避免出現不肯定結果的函數


特定針對主從複製這類業務場景。因爲原理上從庫複製的是主庫執行的語句,使用如 now()、rand()、sysdate()、current_user() 等不肯定結果的函數很容易致使主庫與從庫相應的數據不一致。

另外不肯定值的函數,產生的 SQL 語句沒法利用 query cache。

③多表關聯查詢時,小表在前,大表在後


在 MySQL 中,執行 from 後的表關聯查詢是從左往右執行的(Oracle 相反),第一張表會涉及到全表掃描。


因此將小表放在前面,先掃小表,掃描快效率較高,在掃描後面的大表,或許只掃描大表的前 100 行就符合返回條件並 return 了。


例如:表 1 有 50 條數據,表 2 有 30 億條數據;若是全表掃描表 2,你品,那就先去吃個飯再說吧是吧。

④使用表的別名

當在 SQL 語句中鏈接多個表時,請使用表的別名並把別名前綴於每一個列名上。這樣就能夠減小解析的時間並減小哪些友列名歧義引發的語法錯誤。

⑤用 where 字句替換 HAVING 字句


避免使用 HAVING 字句,由於 HAVING 只會在檢索出全部記錄以後纔對結果集進行過濾,而 where 則是在聚合前刷選記錄,若是能經過 where 字句限制記錄的數目,那就能減小這方面的開銷。


HAVING 中的條件通常用於聚合函數的過濾,除此以外,應該將條件寫在 where 字句中。

where 和 having 的區別:where 後面不能使用組函數。

⑥調整 Where 字句中的鏈接順序


MySQL 採用從左往右,自上而下的順序解析 where 子句。根據這個原理,應將過濾數據多的條件往前放,最快速度縮小結果集。


增刪改 DML 語句優化


①大批量插入數據


若是同時執行大量的插入,建議使用多個值的 INSERT 語句(方法二)。這比使用分開 INSERT 語句快(方法一),通常狀況下批量插入效率有幾倍的差異。

方法一:

insert into T values(1,2); 

insert into T values(1,3); 

insert into T values(1,4);


方法二:

Insert into T values(1,2),(1,3),(1,4); 


選擇後一種方法的緣由有三:
  • 減小 SQL 語句解析的操做,MySQL 沒有相似 Oracle 的 share pool,採用方法二,只須要解析一次就能進行數據的插入操做。

  • 在特定場景能夠減小對 DB 鏈接次數。

  • SQL 語句較短,能夠減小網絡傳輸的 IO。


②適當使用 commit

適當使用 commit 能夠釋放事務佔用的資源而減小消耗,commit 後能釋放的資源以下:
  • 事務佔用的 undo 數據塊。

  • 事務在 redo log 中記錄的數據塊。

  • 釋放事務施加的,減小鎖爭用影響性能。特別是在須要使用 delete 刪除大量數據的時候,必須分解刪除量並按期 commit。


③避免重複查詢更新的數據


針對業務中常常出現的更新行同時又但願得到改行信息的需求,MySQL 並不支持 PostgreSQL 那樣的 UPDATE RETURNING 語法,在 MySQL 中能夠經過變量實現。


例如,更新一行記錄的時間戳,同時但願查詢當前記錄中存放的時間戳是什麼?


簡單方法實現:

Update t1 set time=now() where col1=1

Select time from t1 where id =1;


使用變量,能夠重寫爲如下方式:

Update t1 set time=now () where col1=1 and @now: = now (); 

Select @now


先後兩者都須要兩次網絡來回,但使用變量避免了再次訪問數據表,特別是當 t1 表數據量較大時,後者比前者快不少。


④查詢優先仍是更新(insert、update、delete)優先


MySQL 還容許改變語句調度的優先級,它可使來自多個客戶端的查詢更好地協做,這樣單個客戶端就不會因爲鎖定而等待很長時間。 改變優先級還能夠確保特定類型的查詢被處理得更快。


咱們首先應該肯定應用的類型,判斷應用是以查詢爲主仍是以更新爲主的,是確保查詢效率仍是確保更新的效率,決定是查詢優先仍是更新優先。


下面咱們提到的改變調度策略的方法主要是針對只存在表鎖的存儲引擎,好比  MyISAM 、MEMROY、MERGE,對於 Innodb 存儲引擎,語句的執行是由得到行鎖的順序決定的。

MySQL 的默認的調度策略可用總結以下:
  • 寫入操做優先於讀取操做。

  • 對某張數據表的寫入操做某一時刻只能發生一次,寫入請求按照它們到達的次序來處理。

  • 對某張數據表的多個讀取操做能夠同時地進行。


MySQL 提供了幾個語句調節符,容許你修改它的調度策略:
  • LOW_PRIORITY 關鍵字應用於 DELETE、INSERT、LOAD DATA、REPLACE 和 UPDATE。

  • HIGH_PRIORITY 關鍵字應用於 SELECT 和 INSERT 語句。

  • DELAYED 關鍵字應用於 INSERT 和 REPLACE 語句。


若是寫入操做是一個 LOW_PRIORITY(低優先級)請求,那麼系統就不會認爲它的優先級高於讀取操做。


在這種狀況下,若是寫入者在等待的時候,第二個讀取者到達了,那麼就容許第二個讀取者插到寫入者以前。


只有在沒有其它的讀取者的時候,才容許寫入者開始操做。這種調度修改可能存在 LOW_PRIORITY 寫入操做永遠被阻塞的狀況。

SELECT 查詢的 HIGH_PRIORITY(高優先級)關鍵字也相似。它容許 SELECT 插入正在等待的寫入操做以前,即便在正常狀況下寫入操做的優先級更高。


另一種影響是,高優先級的 SELECT 在正常的 SELECT 語句以前執行,由於這些語句會被寫入操做阻塞。

若是但願全部支持 LOW_PRIORITY 選項的語句都默認地按照低優先級來處理,那麼請使用--low-priority-updates 選項來啓動服務器。


經過使用 INSERTHIGH_PRIORITY 來把 INSERT 語句提升到正常的寫入優先級,能夠消除該選項對單個 INSERT 語句的影響。


查詢條件優化


①對於複雜的查詢,可使用中間臨時表暫存數據

②優化 group by 語句


默認狀況下,MySQL 會對 GROUP BY 分組的全部值進行排序,如 「GROUP BY col1,col2,....;」 查詢的方法如同在查詢中指定 「ORDER BY col1,col2,...;」 。


若是顯式包括一個包含相同的列的 ORDER BY 子句,MySQL 能夠絕不減速地對它進行優化,儘管仍然進行排序。


所以,若是查詢包括 GROUP BY 但你並不想對分組的值進行排序,你能夠指定 ORDER BY NULL 禁止排序。


例如:

SELECT col1, col2, COUNT(*) FROM table GROUP BY col1, col2 ORDER BY NULL ;


③優化 join 語句


MySQL 中能夠經過子查詢來使用 SELECT 語句來建立一個單列的查詢結果,而後把這個結果做爲過濾條件用在另外一個查詢中。


使用子查詢能夠一次性的完成不少邏輯上須要多個步驟才能完成的 SQL 操做,同時也能夠避免事務或者表鎖死,而且寫起來也很容易。可是,有些狀況下,子查詢能夠被更有效率的鏈接(JOIN)..替代。

例子:假設要將全部沒有訂單記錄的用戶取出來,能夠用下面這個查詢完成:
SELECT col1 FROM customerinfo WHERE CustomerID NOT in (SELECT CustomerID FROM salesinfo )

若是使用鏈接(JOIN)..來完成這個查詢工做,速度將會有所提高。

尤爲是當 salesinfo 表中對 CustomerID 建有索引的話,性能將會更好,查詢以下:

SELECT col1 FROM customerinfo 
   LEFT JOIN salesinfoON customerinfo.CustomerID=salesinfo.CustomerID 
      WHERE salesinfo.CustomerID IS NULL


鏈接(JOIN)..之因此更有效率一些,是由於 MySQL 不須要在內存中建立臨時表來完成這個邏輯上的須要兩個步驟的查詢工做。


④優化 union 查詢


MySQL 經過建立並填充臨時表的方式來執行 union 查詢。除非確實要消除重複的行,不然建議使用 union all。


緣由在於若是沒有 all 這個關鍵詞,MySQL 會給臨時表加上 distinct 選項,這會致使對整個臨時表的數據作惟一性校驗,這樣作的消耗至關高。

高效:

SELECT COL1, COL2, COL3 FROM TABLE WHERE COL1 = 10 

UNION ALL 

SELECT COL1, COL2, COL3 FROM TABLE WHERE COL3= 'TEST';


低效:

SELECT COL1, COL2, COL3 FROM TABLE WHERE COL1 = 10 

UNION 

SELECT COL1, COL2, COL3 FROM TABLE WHERE COL3= 'TEST';


⑤拆分複雜 SQL 爲多個小 SQL,避免大事務

以下:
  • 簡單的 SQL 容易使用到 MySQL 的 QUERY CACHE。

  • 減小鎖表時間特別是使用 MyISAM 存儲引擎的表。

  • 可使用多核 CPU。


⑥使用 truncate 代替 delete


當刪除全表中記錄時,使用 delete 語句的操做會被記錄到 undo 塊中,刪除記錄也記錄 binlog。


當確認須要刪除全表時,會產生很大量的 binlog 並佔用大量的 undo 數據塊,此時既沒有很好的效率也佔用了大量的資源。


使用 truncate 替代,不會記錄可恢復的信息,數據不能被恢復。也所以使用 truncate 操做有其極少的資源佔用與極快的時間。另外,使用 truncate 能夠回收表的水位,使自增字段值歸零。

⑦使用合理的分頁方式以提升分頁效率


使用合理的分頁方式以提升分頁效率 針對展示等分頁需求,合適的分頁方式可以提升分頁的效率。


案例 1:

select * from t where thread_id = 10000 and deleted = 0 
   order by gmt_create asc limit 015;

上述例子經過一次性根據過濾條件取出全部字段進行排序返回。數據訪問開銷=索引 IO+索引所有記錄結果對應的表數據 IO。

所以,該種寫法越翻到後面執行效率越差,時間越長,尤爲表數據量很大的時候。


適用場景:當中間結果集很小(10000 行如下)或者查詢條件複雜(指涉及多個不一樣查詢字段或者多表鏈接)時適用。


案例 2:
select t.* from (select id from t where thread_id = 10000 and deleted = 0
   order by gmt_create asc limit 015) a, t 
      where a.id = t.id;

上述例子必須知足 t 表主鍵是 id 列,且有覆蓋索引 secondary key:(thread_id, deleted, gmt_create)。


經過先根據過濾條件利用覆蓋索引取出主鍵 id 進行排序,再進行 join 操做取出其餘字段。


數據訪問開銷=索引 IO+索引分頁後結果(例子中是 15 行)對應的表數據 IO。 所以,該寫法每次翻頁消耗的資源和時間都基本相同,就像翻第一頁同樣。


適用場景: 當查詢和排序字段(即 where 子句和 order by 子句涉及的字段)有對應覆蓋索引時,且中間結果集很大的狀況時適用。

建表優化


在表中創建索引,優先考慮 where、order by 使用到的字段。


儘可能使用數字型字段(如性別,男:1 女:2),若只含數值信息的字段儘可能不要設計爲字符型,這會下降查詢和鏈接的性能,並會增長存儲開銷。


這是由於引擎在處理查詢和鏈接時會 逐個比較字符串中每個字符,而對於數字型而言只須要比較一次就夠了。


查詢數據量大的表 會形成查詢緩慢。主要的緣由是掃描行數過多。這個時候能夠經過程序,分段分頁進行查詢,循環遍歷,將結果合併處理進行展現。


要查詢 100000 到 100050 的數據,以下:
SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY ID ASCAS rowid,* 
   FROM infoTab)t WHERE t.rowid > 100000 AND t.rowid <= 100050


用 varchar/nvarchar 代替 char/nchar。

儘量的使用 varchar/nvarchar 代替 char/nchar ,由於首先變長字段存儲空間小,能夠節省存儲空間,其次對於查詢來講,在一個相對較小的字段內搜索效率顯然要高些。


不要覺得 NULL 不須要空間,好比:char(100) 型,在字段創建時,空間就固定了, 無論是否插入值(NULL 也包含在內),都是佔用 100 個字符的空間的,若是是 varchar 這樣的變長字段, null 不佔用空間。


做者:_陳哈哈

編輯:陶家龍

出處:https://sohu.gg/FGG98i


在公衆號菜單中可自行獲取專屬架構視頻資料,包括不限於 java架構、python系列、人工智能系列、架構系列,以及最新面試、小程序、大前端均無私奉獻,你會感謝個人哈




往期熱門文章:

1,架構的本質:如何打造一個有序的系統?
2, 分佈式高可靠之負載均衡,今天看了你確定會
3, 分佈式數據之緩存技術,一塊兒來揭開其神祕面紗
4,分佈式數據複製技術,今天就教你真正分身術
5, 數據分佈方式之哈希與一致性哈希,我就是個神算子
分佈式存儲系統三要素,掌握這些就離成功不遠了
想要設計一個好的分佈式系統,必須搞定這個理論
分佈式通訊技術之發佈訂閱,乾貨滿滿
9, 分佈式通訊技術之遠程調用:RPC
10  秒殺系統每秒上萬次下單請求,咱們該怎麼去設計



本文分享自微信公衆號 - 架構師修煉(jiagouxiulian)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索