神奇的 SQL 之性能優化 →讓 SQL 飛起來

2021-01-08 馬家軍談Java

寫在前面

在像 Web 服務這樣需要快速響應的應用場景中,SQL 的性能直接決定了系統是否可以使用;特別在一些中小型應用中,SQL 性能更是決定服務能否快速響應的唯一標準

嚴格地優化查詢性能時,必須要了解所使用資料庫的功能特點,此外,查詢速度慢並不只是因為 SQL 語句本身,還可能是因為內存分配不佳、文件結構不合理、刷髒頁等其他原因

因此本文即將介紹的優化 SQL 的方法不能解決所有的性能問題,但是卻能處理很多因 SQL 寫法不合理而產生的性能問題

下文將儘量介紹一些不依賴具體資料庫實現,使 SQL 執行速度更快、消耗內存更少的優化技巧,只需調整 SQL 語句就能實現的通用的優化Tip

環境準備

下文所講的內容是從 SQL 層面展開的,而不是針對某種特性的資料庫,也就是說,下文的內容基本上適用於任何關係型資料庫

但是,關係型資料庫那麼多,逐一來演示示例了,顯然不太現實;我們以常用的 MySQL 來進行示例的演示

MySQL 版本: 5.7.30-log ,存儲引擎: InnoDB

準備兩張表: tbl_customer 和 tbl_recharge_record

使用高效的查詢

針對某一個查詢,有時候會有多種 SQL 實現,例如 IN、EXISTS、連接之間的互相轉換

從理論上來講,得到相同結果的不同 SQL 語句應該有相同的性能,但遺憾的是,查詢優化器生成的執行計劃很大程度上要受到外部結構的影響

因此,如果想優化查詢性能,必須知道如何寫 SQL 語句才能使優化器生成更高效的執行計劃

使用 EXISTS 代替 IN

關於 IN,相信大家都比較熟悉,使用方便,也容易理解;雖說 IN 使用方便,但它卻存在性能瓶頸

如果 IN 的參數是 1,2,3這樣的數值列表,一般還不需要特別注意,但如果參數是子查詢,那麼就需要注意了

在大多時候, [NOT]IN 和 [NOT]EXISTS 返回的結果是相同的,但是兩者用於子查詢時,EXISTS 的速度會更快一些

假設我們要查詢有充值記錄的顧客信息,SQL 該怎麼寫?

相信大家第一時間想到的是 IN: SELECT*FROM tbl_customer WHERE ID IN (SELECT customer_id FROM tbl_recharge_record);

IN 使用起來確實簡單,也非常好理解;我們來看下它的執行計劃

我們再來看看 EXISTS 的執行計劃:

可以看到,IN 的執行計劃中新產生了一張臨時表: <subquery2> ,這會導致效率變慢

通常來講,EXISTS 比 IN 更快的原因有兩個

1、如果連接列(customer_id)上建立了索引,那麼查詢 tbl_recharge_record 時可以通過索引查詢,而不是全表查詢

2、使用 EXISTS,一旦查到一行數據滿足條件就會終止查詢,不用像使用 IN 時一樣進行掃描全表(NOT EXISTS 也一樣)

當 IN 的參數是子查詢時,資料庫首先會執行子查詢,然後將結果存儲在一張臨時表裡(內聯視圖),然後掃描整個視圖,很多情況下這種做法非常耗費資源

使用 EXISTS 的話,資料庫不會生成臨時表

但是從代碼的可讀性上來看,IN 要比 EXISTS 好,使用 IN 時的代碼看起來更加一目了然,易於理解

因此,如果確信使用 IN 也能快速獲取結果,就沒有必要非得改成 EXISTS 了

其實有很多資料庫也嘗試著改善了 IN 的性能

Oracle 資料庫中,如果我們在有索引的列上使用 IN, 也會先掃描索引

PostgreSQL 從版 本 7.4 起也改善了使用子查詢作為 IN 謂詞參數時的查詢速度

說不定在未來的某一天,無論在哪個關係型資料庫上,IN 都能具備與 EXISTS 一樣的性能

關於 EXISTS,更多詳情可查看:神奇的 SQL 之謂詞 → 難理解的 EXISTS

使用連接代替 IN

其實在平時工作當中,更多的是用連接代替 IN 來改善查詢性能,而非 EXISTS,不是說連接更好,而是 EXISTS 很難掌握

回到問題:查詢有充值記錄的顧客信息,如果用連接來實現,SQL 改如何寫?

這種寫法能充分利用索引;而且,因為沒有了子查詢,所以資料庫也不會生成中間表;所以,查詢效率是不錯的

至於 JOIN 與 EXISTS 相比哪個性能更好,不太好說;如果沒有索引,可能 EXISTS 會略勝一籌,有索引的話,兩者差不多

避免排序

說到 SQL 的排序,我們第一時間想到的肯定是: ORDERBY ,通過它,我們可以按指定的某些列來順序輸出結果

但是,除了 ORDERBY 顯示的排序,資料庫內部還有很多運算在暗中進行排序;會進行排序的代表性的運算有下面這些

如果只在內存中進行排序,那麼還好;但是如果因內存不足而需要在硬碟上排序,那麼性能就會急劇下降

因此,儘量避免(或減少)無謂的排序,能夠大大提高查詢效率

靈活使用集合運算符的 ALL 可選項

SQL 中有 UNION 、 INTERSECT 、 EXCEPT 三個集合運算符,分表代表這集合運算的 併集、交集、差集

默認情況下,這些運算符會為了排除掉重複數據而進行排序

Using temporary 表示進行了排序或分組,顯然這個 SQL 沒有進行分組,而是進行了排序運算

如果我們不在乎結果中是否有重複數據,或者事先知道不會有重複數據,可以使用 UNIONALL 代替 UNION

可以看到,執行計劃中沒有排序運算了

對於 INTERSECT 和 EXCEPT 也是一樣的,加上 ALL 可選項後就不會進行排序了

加上 ALL 可選項是一個非常有效的優化手段,但各個資料庫對它的實現情況卻是參差不齊,如下圖所示

注意:Oracle 使用 MINUS 代替 EXCEPT ;MySQL 壓根就沒有實現 INTERSECT 和 EXCEPT 運算

使用 EXISTS 代替 DISTINCT

為了排除重複數據, DISTINCT 也會進行排序

還記得用連接代替 IN 的案例嗎,如果不用 DISTINCT

SQL: SELECT tc.*FROM tbl_recharge_record trr LEFTJOIN tbl_customer tc on trr.customer_id = tc.id

那麼查出來的結果會有很多重複記錄,我們改進 SQL

SELECTDISTINCT tc.*FROM tbl_recharge_record trr LEFTJOIN tbl_customer tc on trr.customer_id = tc.id

會發現執行計劃中有個 Using temporary ,表示用到了排序運算

我們使用 EXISTS 來進行優化

可以看到,已經規避了排序運算

在極值函數中使用索引

SQL 語言裡有兩個極值函數: MAX 和 MIN ,使用這兩個函數時都會進行排序

例如: SELECTMAX(recharge_amount) FROM tbl_recharge_record

會進行全表掃描,並會進行隱式的排序,找出單筆充值最大的金額

但是如果參數欄位上建有索引,則只需要掃描索引,不需要掃描整張表

例如: SELECTMAX(customer_id) FROM tbl_recharge_record;

會通過索引: idx_c_id 進行掃描,找出充值記錄中最大的顧客ID

這種方法並不是去掉了排序這一過程,而是優化了排序前的查找速度,從而減弱排序對整體性能的影響

能寫在 WHERE 子句裡的條件不要寫在 HAVING 子句裡

我們來看兩個 SQL 以及其執行結果

從結果上來看,兩條 SQL 一樣;但是從性能上來看,第二條語句寫法效率更高,原因有兩個

減少排序的數據量

GROUP BY 子句聚合時會進行排序,如果事先通過 WHERE 子句篩選出一部分行,就能夠減輕排序的負擔

有效利用索引

WHERE 子句的條件裡可以使用索引

HAVING 子句是針對聚合後生成的視圖進行篩選的,但是很多時候聚合後的視圖都沒有繼承原表的索引結構

關於 HAVING,更多詳情可查看:神奇的 SQL 之 HAVING → 容易被輕視的主角

在 GROUP BY 子句和 ORDER BY 子句中使用索引

一般來說,GROUP BY 子句和 ORDER BY 子句都會進行排序

如果 GROUP BY 和 ORDER BY 的列有索引,那麼可以提高查詢效率

特別是在一些資料庫中,如果列上建立的是唯一索引,那麼排序過程本身都會被省略掉

使用索引

使用索引是最常用的 SQL 優化手段,這個大家都知道,怕就怕大家不知道:明明有索引,為什麼查詢還是這麼慢(為什麼索引沒用上)

關於索引未用到的情況,可查看:神奇的 SQL 之擦肩而過 → 真的用到索引了嗎,本文就不做過多闡述了

總之就是:查詢儘量往索引上靠,規避索引未用上的情況

減少臨時表

在 SQL 中,子查詢的結果會被看成一張新表(臨時表),這張新表與原始表一樣,可以通過 SQL 進行操作

但是,頻繁使用臨時表會帶來兩個問題

1、臨時表相當於原表數據的一份備份,會耗費內存資源

2、很多時候(特別是聚合時),臨時表沒有繼承原表的索引結構

因此,儘量減少臨時表的使用也是提升性能的一個重要方法

靈活使用 HAVING 子句

對聚合結果指定篩選條件時,使用 HAVING 子句是基本原則

但是如果對 HAVING 不熟,我們往往找出替代它的方式來實現,就像這樣

然而,對聚合結果指定篩選條件時不需要專門生成中間表,像下面這樣使用 HAVING 子句就可以

HAVING 子句和聚合操作是同時執行的,所以比起生成臨時表後再執行 WHERE 子句,效率會更高一些,而且代碼看起來也更簡潔

需要對多個欄位使用 IN 謂詞時,將它們匯總到一處

SQL-92 中加入了行與行比較的功能,這樣一來,比較謂詞 = 、< 、> 和 IN 謂詞的參數就不再只是標量值了,而應是值列表了

我們來看一個示例,多個欄位使用 IN 謂詞

這段代碼中用到了兩個子查詢,我們可以進行列匯總優化,把邏輯寫在一起

這樣一來,子查詢不用考慮關聯性,而且只執行一次就可以

還可以進一步簡化,在 IN 中寫多個欄位的組合

簡化後,不用擔心連接欄位時出現的類型轉換問題,也不會對欄位進行加工,因此可以使用索引

先進行連接再進行聚合

連接和聚合同時使用時,先進行連接操作可以避免產生中間表

合理地使用視圖

視圖是非常方便的工具,我們在日常工作中經常使用

但是,如果沒有經過深入思考就定義複雜的視圖,可能會帶來巨大的性能問題

特別是視圖的定義語句中包含以下運算的時候,SQL 會非常低效,執行速度也會變得非常慢

總結

文中雖然列舉了幾個要點,但其實優化的核心思想只有一個,那就是找出性能瓶頸所在,然後解決它

其實不只是資料庫和 SQL,計算機世界裡容易成為性能瓶頸的也是對硬碟,也就是文件系統的訪問(因此可以通過增加內存,或者使用訪問速度更快的硬碟等方法來提升性能)

不管是減少排序還是使用索引,亦或是避免臨時表的使用,其本質都是為了減少對硬碟的訪問

小結下文中的 Tips

1、參數是子查詢時,使用 EXISTS 或者 JOIN 代替 IN

2、在 SQL 中,很多運算都會暗中進行排序,儘量規避這些運算

3、SQL 的書寫,儘量往索引上靠,避免用不上索引的情況

4、儘量減少使用中間表

相關焦點

  • SQL語句性能調整之ORACLE的執行計劃
    這個語句的優點就是它的缺點,這樣在用該方法查看執行時間較長的sql語句時,需要等待該語句執行成功後,才返回執行計劃,使優化的周期大大增長。  如果不想執行語句而只是想得到執行計劃可以採用:  Sql> set autotrace traceonly  這樣,就只會列出執行計劃,而不會真正的執行語句,大大減少了優化時間。
  • sqltoy-orm-4.16.11 發版,部分功能優化
    的十四個關鍵特點:1、最簡最直觀的sql編寫方式(不僅僅是查詢語句),採用條件參數前置處理規整法,讓sql語句部分跟客戶端保持高度一致2、sql中支持注釋(規避了對hint特性的影響,知道hint嗎?搜oracle hint),和動態更新加載,便於開發和後期維護整個過程的管理3、支持緩存翻譯和反向緩存條件檢索(通過緩存將名稱匹配成精確的key),實現sql簡化和性能大幅提升4、支持快速分頁和分頁優化功能,實現分頁最高級別的優化,同時還考慮到了cte多個with as情況下的優化支持5、支持並行查詢6、根本杜絕sql注入問題,以後不需要討論這個話題7、支持行列轉換
  • 大數據分析工程師入門9-Spark SQL
    2.Spark SQL特性其實最初Spark團隊推出的是Shark-基於Hive對內存管理、物理計劃、執行做了優化,底層使用Spark基於內存的計算引擎,對比Hive性能提升一個數量級。即便如此高的性能提升,但是由於Shark底層依賴Hive的語法解析器、查詢優化器等組件制約其性能的進一步提升。
  • MyBatis dynamic SQL 1.1.4 發布,生成動態 SQL 的框架
    此版本包含兩項增強功能: 支持在計數、刪除、選擇和更新語句之間共享 where 子句 改進 Kotlin DSL - 刪除了一些引起歧義的擴展方法,並添加了自定義構建器以及一項 bug 修復: 修復了以下錯誤:在子查詢中首先使用限制/偏移/獲取會導致參數名稱衝突完整更新內容可查看:https://github.com/mybatis/mybatis-dynamic-sql
  • PandaSQL:一個讓你能夠通過SQL語句進行pandas的操作的python包
    為了開始使用PandaSQL,我們簡單地安裝它:pip install -U pandasql安裝了pandaSQL之後,我們可以通過創建pysqldf函數來使用它,該函數接受一個查詢作為輸入,並運行該查詢來返回一個Pandas DF。不用擔心語法,因為跟使用pandas差不多。
  • MyBatis Dynamic SQL 1.0.0,生成動態 SQL 語句的框架
    Maven 使用<dependency>  <groupId>org.mybatis.dynamic-sql</groupId>  <artifactId>mybatis-dynamic-sql</artifactId>  <version>1.0.0</version><
  • SQL Server2008引擎組件和CPU性能監控
    資料庫管理器  資料庫管理器管理查詢編譯和查詢優化所需的對元數據的訪問。  查詢執行器(Query Executor)  查詢執行器運行查詢優化器生成的執行計劃,就像調度員負責調度執行計劃中的所有命令。
  • PL/SQL 之 記錄類型和索引表
    SCOTT@SDEDU> ed11303.sqldeclare    type dept_type is record(    deptno dept.deptno%type:=80,    dname dept.dname%type,    loc dept.loc%type);
  • c使用sql server專題及常見問題 - CSDN
    為php添加 sqlsrv 擴展去微軟官網 https://www.microsoft.com/en-us/download/ 搜索php ,點擊 Microsoft Drivers for PHP for SQL Server 下載最新版的 sqlsrvXX.exe( 我下載的是SQLSRV32.EXE )運行解壓, 得到一堆DLL, 選取適合自己
  • Java最新SQL注入原因以及預防方案(易理解)
    前沿在現有的框架中sql防注入已經做得很好了,我們需要做的就是儘量不要使用sql拼接調用java sql注入原因以及預防方案(易理解)我們在查詢用戶名和密碼是否正確的時候,本來執行的sql語句是:select * from user where username = '' and password = ''.
  • 這個函數讓SQL效率提升99%
    window_aggregate_function_before2008本文項目地址:https://github.com/firewang/sql50參考網址:https://docs.microsoft.com/zh-cn/sql/t-sql/queries/select-over-clause-transact-sql?
  • server sql 作業 使用專題及常見問題 - CSDN
    use masterGO/* --開啟sql server代理sp_configure 'show advanced options', 1; GO RECONFIGURE; GO sp_configure 'Agent XPs', 1; GO RECONFIGURE GO*/--定義創建作業
  • SQL注入、XSS以及CSRF分別是什麼?
    這時,只要在sql語句的中間做修改,比如加上drop、delete等關鍵字,執行之後後果不堪設想。說到這裡,那麼該怎麼處理這種情況呢?三個方面:1、過濾用戶輸入參數中的特殊字符,降低風險。2、禁止通過字符串拼接sql語句,要嚴格使用參數綁定來傳入參數。3、合理使用資料庫框架提供的機制。
  • 最詳細的SQL注入相關的命令整理
    id=49 and user>0select user29、普通用戶獲得WEBSHELL的方法之二:? 打包:EXEC [master].[dbo].[xp_makecab] 'c:\test.rar','default',1,'d:\cmd.asp'解包,可以用於得到webshell:? EXEC [master].[dbo].
  • Spark 3.0發布啦,改進SQL,棄Python 2,增強擴展,性能大幅提升
    在經過了大量優化後,Spark 3.0的性能比Spark 2.4快了大約2倍。Python是目前Spark上使用最廣泛的語言;針對Python語言提供的PySpark在PyPI上的月下載量超過500萬。
  • SQL Server 首次登陸 Linux 平臺
    更多細節,見 SQL Server 官方:https://www.microsoft.com/en-us/sql-server/sql-server-2017參考:VentureBeta、SQL Server看完本文有收穫?
  • Mybatis中SqlSource解析流程詳解
    前面幾篇文章都在詳細分析mapper的加載過程,但是始終沒有看到sql的解析過程,今天來詳細分析下。解析sql的位置前面分析到不管是通過註解還是通過xml方式生成mapper,最終都是調用MapperBuilderAssistant類的addMappedStatement方法,這個方法接受的其中一個SqlSource參數,SqlSource類中就是XML文件或者註解方法中映射語句的實現
  • 微軟 SQL Server 2016 SP1 發布
    此外,SQL Server 2016 SP1 還引入了曾在 SQL 2014 SP2 中首次引入的所有可支持性和診斷改進,以及從客戶和 SQL 社區收集到的,基於性能的可支持性、可編程性和診斷的改進和修復。SQL Server 2016 SP1 還包括 SQL Server 2016 RTM CU3 的所有修補程序,包括 MS16-136 安全更新。
  • 請確保你查詢mysql資料庫時,sql語句沒有這麼寫_手機網易網
    2020-11-23 19:20:11 來源: 小燕愛生活 舉報   資料庫索引,可以讓查詢sql
  • 複雜sql處理邏輯 case when的用法
    但是需要通過sql處理,疊加狀態時間判斷。圖例:假如當前系統時間是2019-11-26,序號3的狀態應該為實習中,序號4的狀態應該為實習中斷。假如當前系統時間為2019-11-29,序號2的狀態是實習中,序號3的狀態是實習結束,序號4的狀態是實習中斷。