高性能MySQL優化要點整合

本篇文章爲《高性能MySQL》一書的讀書筆記,主要提取了幾點在開發中會用到的高性能的做法與一些MySQL的介紹

1. MySQL架構

MySQL最重要、最與衆不同的特性是它的存儲引擎架構。這種架構的設計將查詢處理及其他系統任務和數據的存儲/提取相分離。此時可以根據性能、特性以及其他需求來定製化數據存儲的方式(存儲引擎)

1.1 MySQL邏輯架構

在這裏插入圖片描述

服務器邏輯架構圖,截取自《高性能MySQL》

  • 中間那層架構爲MySQL核心部分,負責查詢解析、分析、優化、緩存以及內置函數(日期、時間、數學等),所有跨存儲引擎的功能都在這一層實現:存儲過程、觸發器、視圖等
  • 最下面一層存儲引擎層負責數據的存儲和提取,每個存儲引擎都有各自的優勢和劣勢。服務器通過API與存儲引擎進行通信。存儲引擎API包含了幾十個底層函數,用於執行諸如“開始一個事務”等操作。存儲引擎不會去解析SQL,只是簡單的響應上層服務器的請求

2. Schema與數據類型優化

應該根據系統將要執行的查詢語句來設計schema,需要權衡各種因素。例如反泛式的設計可以加快某些查詢,但可能使另一個查詢變慢。

2.1 數據類型的選擇

2.1.1 整數類型

INT(1) 和 INT(20) 在存儲與計算上是相同的,只不過其規定了顯示字符的個數

存儲整數,有如下類型(包括類型所佔的存儲空間,間接代表了其值的範圍)

  • TINYINT(8 bit)
  • SMALLINT(16 bit)
  • MEDIUMINT(24 bit)
  • INT(32 bit)
  • BIGINT(64 bit)

整數類型有可選的UNSIGNED屬性,表示無符號,如無符號則存儲的值範圍擴大一倍(乘2)

2.1.2 字符串類型

VARCHAR

用於存儲可變長字符串,需要多消耗1到2的額外字節來存儲字符串(記錄長度),節省了存儲空間,對性能有幫助,但是如果UPDATE使得行變得比之前更長,InnoDB需要分裂頁來使行可以放入頁內,造成一定的碎片化

下面情況使用VARCHAR是合適的:

  • 字符串列的最大長度比平均長度大很多,充分發揮了變長字符串的節省能力
  • 列的更新很少,這樣碎片化就少了

CHAR

MySQL總是根據定義的字符串長度分配空間。對於固定的列,存儲空間也比VARCHAR小1到2個字節(記錄長度)

下面情況使用CHAR是合適的:

  • MD5值,固定的長度,更少的存儲空間
  • 很短的字符串,或者所有值都接近同一個長度
  • 經常變更的值,不易產生碎片

VARCHAR(5) 和 VARCHAR(200) 存儲的空間開銷是一樣的,但更長的列會消耗更多的內存(或磁盤),體現在使用內存臨時表(或磁盤臨時表)進行排序或操作時。

BLOB和TEXT

爲存儲很大的數據而設計的字符串數據類型

  • MySQL把這兩個當作一個獨立的對象處理,如果太大,InnoDB會使用專門的外部存儲區域來進行存儲,此時每個值在行內就存儲1-4個字節的指針
  • 兩者的不同是BLOB存儲的是二進制數據,沒有排序規則和字符集,而TEXT有
  • 不能將這兩列全部長度進行索引,進而沒有覆蓋索引和索引消除的排序

使用枚舉代替字符串類型

枚舉列把一些不重複的字符串存儲成一個預定義的集合,MySQL在存儲枚舉時非常緊湊,會根據列表值的數量壓縮到一到兩個字節中。

  • MySQL在內部會將每個值在列表中的位置保存爲整數,並且在.frm文件中保存數字-字符串的映射關係(查找表),所以在查找時有一些開銷,在其與CHAR/VARCHAR列關聯時體現,所以有與字符串類型做關聯時最好使用字符串(或者枚舉與枚舉進行關聯,會更加的快)
    • 有時候枚舉會大大減少表的大小,所以有時候關聯的開銷也是值得的
  • 缺點是字符串列表是固定的,添加元素只能在列表末尾添加

2.1.3 日期和時間類型

DATATIME

保存大範圍的值,從1001年到9999年,精度爲秒,格式爲YYYYMMDDHHMMSS的整數,使用8個字節的存儲空間

TIMESTAMP

時間戳,保存了從1970年1月1日以來的秒數,與UNIX時間戳相同,只使用4個字節的存儲空間,只能表示1970到2038年

2.1.4 位數據類型

少數幾種存儲類型使用緊湊的位存儲數據,底層來講都是字符串類型

BIT

在InnoDB中,爲每個BIT列使用一個足夠存儲的最小整數類型來存放,所以其在InnoDB中不能節省空間,並且結果有時可能令人費解(存儲的二進制查找出來的是字符碼對應的字符串,例如57的二進制,查找出來會變爲"9"),應該謹慎使用BIT類型,如果存儲一個true/false值,可以使用CHAR類型,節省了空間又不會令人費解

SET

通過一系列打包的位集合,可以保存很多個true/false的值,有效利用存儲空間,例如一條記錄裏有一列權限,其中有是否可讀,是否可寫,是否可刪除,一系列的是否,通過指定位的0/1來達到

2.1.5 選擇標識符

標識符即爲一行數據中唯一標識此行的列,需要確保所有關聯表中的此標識符都使用同樣的類型,包括UNSIGNED這樣的屬性

  • 整數類型
    • 通常是標識列的最好選擇,它們很快並且可以使用AUTO_INCREMENT
  • ENUM和SET
    • 糟糕的選擇,此類型是固化的視圖,如果確定表主鍵只有固定那麼幾個的話,也可以選擇
  • 字符串類型
    • 應該避免使用字符串類型作爲標識列,它們很消耗空間,通常比數字類型慢
    • 多加註意“隨機”的字符串,如MD5、SHA1、UUID等等
      • 插入時會隨機寫索引的不同位置,使得INSERT更慢,導致頁分裂、磁盤隨機訪問
      • SELECT更慢,因爲邏輯相鄰的列分佈在物理地址的不同位置

2.2 MySQL的schema設計

2.2.1 範式與反範式

範式化的優點:

  • 更新操作比反範式快,沒有冗餘數據,只需要修改最少的數據
  • 範式化的表通常更小,可以更好的放入內存,執行操作更快
  • 幾乎沒有冗餘數據代表着更少的DISTINCT或者GROUP BY語句

範式化的缺點:

  • 查詢時通常需要關聯表,才能拿到一個完整需要的數據信息
  • 有時候不同列在不同的表中,原本這些列只需要利用一次索引就可以完成查詢

反範式化的優點:

  • 所有數據都在一張表中,避免了關聯操作,如果不需要關聯表,最差情況下(全表掃描),當數據量比內存大時可能比關聯快得多,因爲是順序IO
  • 在一些查詢中,使用一個索引就能得到所有的列,有些查詢得益於此變得比關聯快得多

反範式化的缺點:

  • 列數據冗餘存儲,更新數據時可能需要更新多表,更新變慢變複雜
  • 行信息不明確,例如用戶表與用戶發送的消息表合併在一起,通常是很糟糕的

2.2.2 混用範式化與反範式化

  • 沒有絕對的範式和反範式
  • 有時候適當冗餘某個列(反範式),可以增加查詢效率,可以利用索引避免排序操作,一次索引就可以查出所有數據,但相對更新操作代價就高了,需要同時更新所有有冗餘數據的表,需要權衡更新與查詢

2.2.3 計數器表

有時候需要保存一個計數,來表示例如文件的下載次數等等,創建一張獨立的表存儲計數器是一個好主意

  • 同時更新次數時UPDATE會有一個全局的互斥鎖,這樣計數就是串行執行,在高併發下效率很低,可以預先添加例如100個槽,在計數時在100中隨機一個數,在隨機數的那一行執行UPDATE,這樣就可以增加並行性,要獲得結果,需要SUM函數聚合計算全部的100個槽

爲了提升讀,經常會建一些額外索引,增加冗餘列,增加緩存,這些方法都會增加寫的負擔,開發難度也隨即增加

3. 創建高性能的索引

索引是存儲引擎用於快速找到記錄的一種數據結構

3.1 B-Tree索引

  • 大大減少了服務器需要掃描的數據量
  • 避免排序和臨時表
  • 將隨機IO變爲順序IO

傳說中的”三星索引“,也即爲索引的最完美追求,如下所示:

  • 索引將相關的記錄放到一起
  • 索引中的數據順序和查找中的排列順序一致
  • 索引的列包含了查詢需要的全部列

3.2 哈希索引

在MySQL中,只有Memory引擎支持哈希索引。Memory引擎同時也支持B-Tree索引

  • 哈希不是順序存儲,無法用於排序
  • 不支持匹配查找,如(A,B)索引不能用在只有A的查詢
  • 只能用在等值比較(=、IN()、<=>)
  • 查找數據非常快

在InnoDB中,若索引列非常長,則可以使用自定義的哈希索引,例如存儲URL列,使用其做索引會非常大,如果只是將其哈希,放到一個名爲 url_crc 這列,保證哈希之後字符串不大,並且將其作爲索引,那麼下次就可以使用哈希方式進行查詢:

SELECT id FROM url WHERE url=“https://blog.csdn.net/qq_41737716” AND url_crc=‘d23hdi23dh’

這樣,利用url_crc的索引+URL的哈希,查詢會非常快,同時保證了索引不會變的特別大

3.3 高性能索引策略

只討論InnoDB的B+Tree索引

3.3.1 前綴索引選擇

有時候索引列比較長,這讓索引變的大且慢,一個策略是之前提到過的自定義哈希索引,還有就是如下的策略:

  • 選擇足夠長的索引列保證離散性(比如一列只有男/女,離散性相當差)
    • 計算離散性
      • SELECT COUNT(DISTINCT city)/COUNT(*) FROM city
      • SELECT COUNT(DISTINCT LEFT(city, 3))/COUNT(*) FROM city
      • SELECT COUNT(DISTINCT LEFT(city, 4))/COUNT(*) FROM city
    • 創建前綴索引(前綴7)
      • ALTER TABLE city ADD KEY (city(7))
  • 注意,無法使用前綴索引做order by 和 group by 以及 覆蓋索引

3.3.2 選擇合適的索引列順序

(多列索引)好的索引列順序,可以滿足order by 和 group by 以及 distinct等查詢需求

  • 當不需要考慮排序和分組時,將選擇性(離散性)最高的列放在前面,此時索引的作用只是用於WHERE條件的查找,過濾出足夠多的行
    • 使用上面的計算離散性公式來計算
  • 在一些情況下,也需要根據運行頻率最高的查詢來調整順序

3.4 聚簇索引

優點:

  • 將相關數據保存在一起,只需讀取少數的數據頁,如果沒有,則一行數據需要一次磁盤I/O
  • 數據訪問更快
  • 覆蓋索引時可以直接使用葉節點的主鍵值(二級索引本身加主鍵值也能達到覆蓋索引)

缺點:

  • 插入速度嚴重依賴插入順序,主鍵順序插入時最快方式,如果不是順序,最好使用OPTIMIZE TABLE命令重新組織一下表
  • 二級索引可能比想象中要大(因爲葉子節點是主鍵列)
  • 二級索引訪問需要兩次查找

3.4.1 在InnoDB中按主鍵順序插入

最好避免隨機的聚簇索引,例如使用UUID來作爲聚簇索引則會很糟糕,具體請看以下測試

來源於《高性能MySQL》
在這裏插入圖片描述

可以看到,使用順序主鍵插入與uuid隨機主鍵插入,在100萬數據時差別是40多秒,在插入100萬數據後,再插入300萬數據,差別就相當巨大了,需要做一次 OPTIMIZE TABLE 來重建表並優化頁的填充

3.5 使用索引掃描來做排序

索引天生自帶順序,如果能利用索引避免排序,將提升性能,如果沒有用到索引,而是外部排序(磁盤或者內存),則EXPLAIN執行計劃中EXTRA列將會是filesort

  • order by中的列需要滿足索引的最左前綴要求即可

3.6 冗餘索引

冗餘索引指的是,如果有索引 (A,B),再創建 (A) 索引就是冗餘索引,此時(A,B) 索引可以當作A使用。如果是 (B,A) 或 (B) 就不算冗餘,值得一提的是 (A, ID) 也是冗餘的。

  • 大多數情況下都不需要冗餘索引,應該儘量擴展已有索引而不是創建新索引。但也有時候處於性能方面考慮需要冗餘索引,因爲有時候擴展已有的索引會導致其變的太大,從而影響其他使用該索引的查詢性能
  • 冗餘索引將導致插入、更新、刪除操作速度變慢!儘量避免冗餘

3.6.1 索引和鎖

索引可以讓查詢鎖定更少的行,使得表鎖變爲行鎖

  • 索引可以在存儲引擎層面過濾掉無效的行,返回給服務器層時纔會應用WHERE語句,此時纔會鎖定這一部分數據,所以如果索引能過濾掉只剩一行數據的話,此時就是行鎖
  • 如果不能使用索引查找的話可能會很糟糕,MySQL會做全表掃描鎖住所有的行,此時就是表鎖

3.7 索引設計

下面羅列幾個索引設計的要點:

  • 如何建立一個支持多種過濾條件的索引
    • 避免多個範圍條件
      • 如果有範圍查詢要放最後(不超過一個範圍查詢,如果有兩個範圍查詢的列,索引無法使用到後面的索引列了)
    • 什麼放前面
      • 查詢頻率最高的一般放最前面,不考慮排序或組合情況下,選擇性高的也可以往前放
    • 重用同一個索引
      • 如果某列不應用在WHERE子句中,則使用IN補救,例如性別,索引爲(性別,姓名),此時搜索名爲Jack的人,但是不分男女,就可以使用 where 性別 in (‘m’, ‘f’) and 姓名 = jack,同樣可以使用到索引(注意避免不要in太多,每個列都in的話組合情況是相乘的,組合太多影響性能)
  • 有多個範圍條件怎麼辦
    • 需要將某個範圍轉化一個思路,例如需要範圍查找一段年齡範圍和最近7天上線過的用戶,此時可以將7天內這個範圍轉化爲0(7天沒上線)和1(7天內有上線)這個字段,從範圍變爲等值查找,索引(active, age)就可以使用索引了,但需要在用戶上線時更新active這個字段爲1
    • 上述方法無法精確查找到底是幾天上線,此時不妨考慮其中一個範圍條件不加入索引,直接放入where語句,如果這個條件的過濾性不高,即使加入索引幫助也不會太大,所以缺少此索引也不會損失太多性能
  • 優化排序
    • 考慮到查詢是否有排序,需要利用索引消除filesort排序,例如需要排序一個註冊時間字段,並查找所有女性用戶,此時是 where 性別 = 女 order by 註冊時間,此時如果索引是(性別,註冊時間),將會利用到索引過濾不必要掃描的行,並且利用索引直接完成排序,不需要filesort

如果索引優化這條路行不通,看看是否能夠重寫優化查詢語句

4. 查詢性能優化

  • 訪問的數據太多
    • 查詢性能低下最基本的原因是訪問的數據太多,某些查詢不可避免地需要篩選大量數據
  • 請求了不需要的記錄
    • 多表關聯時返回全部列
    • 總是取出所有列(有時)
    • 查詢不需要的記錄
      • 有時候MySQL會返回全部的結果集,然後再進行計算,這樣就會獲取N行,實際上只用到前10行數據
    • 重複查詢相同的數據
      • 這部分可以通過緩存解決,如果重複獲取一些改動不大的數據,這無疑是浪費的
  • 掃描了額外的記錄
    • 可以查看掃描的行數與返回的行數來確認
    • 查看訪問類型(Explain的type)
      • 增加一個合適的索引,來過濾行,以避免掃描過多額外的記錄
    • 一般MySQL能夠使用如下方式應用WHERE條件,從好到壞依次爲:
      • 在索引中使用WHERE過濾記錄(存儲引擎層)
      • 使用覆蓋索引返回記錄,直接從索引中得到記錄(服務器層,無需回表)
      • 從表中返回數據,在服務器層過濾條件(在服務器層,Extra中爲Using Where),這種情況下有可能會有全表掃描但只需要前10條數據的可能,這樣極度浪費性能,體現了一個好的索引的重要性

4.1 重構查詢

  • 有時候,將一個複雜查詢分解爲多個簡單查詢,可能會提升性能
    • 在目前的網絡條件下,多個小查詢的網絡開銷並不大
    • 分解關聯查詢:將一條多關聯查詢語句分解爲多個單查詢語句
      • 緩存效率更高:某些表的數據並不會改動,或是不頻繁改動,此時此條查詢如果緩存下來,就剩下了對此表的查詢
      • 數據可以分佈到不同的MySQL服務器上
      • 可以使用 IN 來代替關聯查詢

4.2 查詢緩存

如果表發生變化,和這個表相關的所有緩存數據都將失效,這意味着查詢緩存並不適用於更新頻繁的表,很多時候都應該默認關閉查詢緩存

  • 有不確定數據時不會被緩存:NOW、CURRENT_DATE函數,所以需要將日期提前計算好
  • 打開緩存對讀與寫都有額外的消耗:
    • 讀前:檢查是否命中緩存
    • 讀後:如果沒有緩存過,查詢之後會放入緩存
    • 寫時:將與對應表的所有緩存都設置失效
      • 如果查詢緩存使用了大量的內存,那麼失效操作就有可能成爲一個問題,失效操作由一個全局鎖保護的,所有檢測是否命中緩存、失效操作都需要等待這個鎖
  • 查詢緩存是完全存在內存中的
    • 管理內存的開銷
    • 放入緩存,分配幾個內存數據塊的開銷(鎖住一段固定空間塊,找到合適大小數據塊)
      • 多次放入緩存的時候難免會產生很多碎片,太小的空間碎片很難被使用到
    • 失效緩存的開銷
  • 在InnoDB引擎中,由於其多版本控制,對錶的修改事務在提交之前,此表的緩存都無法應用,這使得大事務對緩存命中率很不友好
  • 緩存碎片、內存不足、數據修改都會造成緩存失效
  • 如果空閒塊很多,碎片很少,也沒有什麼由於內存導致的緩存失效,但是命中率還很低,那麼很可能說明查詢緩存並沒有什麼好處
  • 在高併發場景下,查詢緩存的表現一般來說都比較差

什麼情況下可以使用查詢緩存

  • 通過觀察打開或者關閉確認是否需要打開緩存
  • 有需要消耗大量資源的查詢才適合緩存(複雜查詢),且UPDATE、DELETE、INSERT相比查詢要少非常多

4.3 查詢優化器

查詢MySQL計算查詢的成本

select * from xxx 之後

SHOW STATUS LIKE ‘Last_query_cost’

表示MySQL的優化器認爲需要多少個數據頁的隨機查找完成查詢

下面是一些MySQL優化器會做優化的部分:

  • 重新定義關聯表的順序
    • MySQL會將這些關聯表中需要掃描函數最少的那張表作爲驅動表進行關聯排序,注意有時候這樣會用不到索引排序(使用STRAIGHT_JOIN來優化此類查詢,大部分情況都不會用到)
  • 優化COUNT、MIN、MAX
    • 最大最小值可以使用索引的最右或最左的數據,如果使用到這一優化,此值將爲常量對待,並且Explain中可以看到 “Select tables optimized away”
    • COUNT 在MyISAM引擎中可以得到優化(維護了一個變量存放行數,無需掃描)
  • 覆蓋索引
  • 列表 IN 的比較
    • in 查詢時,會將其列表中先排序,然後通過二分查找的方式確定值是否在列表中
  • 等等…

關聯查詢-關聯表的順序

  • 有時候,查詢語句的關聯順序會導致索引排序失效,而使用filesort的方式排序
    • MySQL在進行filesort的時候需要使用的臨時存儲空間可能會比想象的大很多,在排序時對每個排序記錄都會分配一個定長空間,這個定長空間必須容納最長的字符串,例如VARCHAR的最大長度
    • 詳情看這一篇筆記,和這一篇筆記
  • 有時候,優化器爲了尋找關聯表需要掃描的行數,可能需要遍歷每個表逐個嵌套循環,如果超過10個表的關聯,這部分將非常耗時,此時優化器將使用貪婪搜索的方式查找最優的關聯順序,但我們也可以做一個排序順序優化,可以提前花大時間計算出最優關聯表順序,然後在語句中加上STRAIGHT_JOIN關鍵字,減少優化器的工作

4.4 關聯子查詢

MySQL的子查詢實現得非常糟糕,最典型的一類查詢是where條件中包含in的子查詢:

  • select * from xx where xid in ( select * from yy where yid = 1)

其會把外層表壓到子查詢中:

  • select * from xx where exists ( select * from yy where yid = 1 and yy.xid = xx.xid)

這樣,子查詢中的語句就無法提前完成,這條語句需要先全表掃描xx表,然後一條一條與yy表匹配。最好改寫成如下方式:

  • select xx.* from xx inner join yy using(xid) where yid = 1

有些情況子查詢會比較快,但大部分情況下最好使用關聯查詢,在一些場景中可能需要測試纔可以知道子查詢和關聯查詢哪個更快一些

4.5 UNION的限制

有時,MySQL無法將限制條件從外層下推到內層,例如如下UNION語句:

  • ( select * from a order by name ) UNION ALL ( select * from b order by name ) limit 20

這樣會把a表的全部記錄和b表的全部記錄存在一個臨時表中,然後從臨時表中取出前20條,需要手動將條件給子查詢加上:

  • ( select * from a order by name limit 20 ) UNION ALL ( select * from b order by name limit 20 ) limit 20

這樣,臨時表就只有40條記錄了,如果希望全局有序,需要在外層再加一個order by

4.6 最大值和最小值優化

對於MIN()、MAX() 查詢,MySQL的優化做的並不好,例如:

  • select min(id) from a where name = ‘jack’

由於nam沒有索引,此時會全表掃描,這很糟糕,通過改寫成如下解決:

  • select id from a use index(primary) where name = ‘jack’ limit 1

4.7 查詢優化器的提示(hint)

  • STRAIGHT_JOIN
    • 放置在select關鍵之之後,或者兩個關聯表之間,前者將所有表的順序定義爲語句中的順序,後者固定前後兩表的關聯順序
  • SQL_CACHE和SQL_NO_CACHE
    • 查詢緩存
  • FOR UPDATE 和 LOCK IN SHARE MODE
    • 控制select語句的鎖機制,很容易造成鎖爭用,並行性下降的問題,此條語句都可以轉換爲UPDATE的形式,應該儘量轉換,在下面會討論到
  • USE INDEX、IGNORE INDEX 和 FORCE INDEX
    • 告訴優化器使用或不使用索引
  • SQL_SMALL_RESULT 和 SQL_BIG_RESULT
    • 只對select語句有效,告訴優化器對group by 或者 distinct查詢如何使用臨時表及排序
    • SQL_SMALL_RESULT:告訴優化器結果集很小,使用內存中的索引臨時表
    • SQL_BIG_RESULT:告訴優化器結果集很大,建議使用磁盤臨時表做排序操作

4.8 優化特定類型的查詢

4.8.1 COUNT查詢

  • 使用下面的查詢解決兩個條件的COUNT計算

    • select count( color = ‘blue’ or null ) as blue, count( color = ‘red’ or null ) as red from item
  • 使用近似值代表總數

    • 有時候count並不需要太過精確,此時可以利用緩存計算一個近似值
  • 通常來說,count都需要掃描大量的行,很難優化

4.8.2 優化關聯查詢

  • 確保ON或者USING子句中的列有索引
    • 若A表與B表用列C關聯,如果關聯順序是B、A,那麼只需要在A表上的列C有索引,B表不需要,多餘的索引會帶來額外的負擔
  • 確保GROUP BY 和 ORDER BY 表達式只涉及一個表中的列,這樣MySQL纔有可能使用索引優化

4.8.3 優化子查詢

儘可能使用關聯查詢代替(MySQL5.6之後可能會優化)

4.8.4 優化GROUP BY 和 DISTINCT

兩者都可以使用索引來優化,當無法使用索引時,GROUP BY 使用兩種策略完成:

  1. 使用臨時表來做分組
  2. 文件排序來做分組

可以通過上面說過的,SQL_SMALL_RESULT 和 SQL_BIG_RESULT 來讓優化器按照你希望的方式運行

  • 最有效的方法是使用索引進行優化

  • 如果需要對關聯查詢做分組,並且按照查找表中某個列分組,通常採用查找表的標識列分組的效率會比其他列更高,例如:

    • select a.first_name, a.last_name, count(*)
      from b
      	inner join a using(aid)
      group by a.first_name, a.last_name
      

      如果將其換做以下寫法,效率會更高

      select a.first_name, a.last_name, count(*)
      from b
      	inner join a using(aid)
      group by b.aid
      -- 或者 group by a.aid 測試結果是更快的
      

      其利用了姓名的唯一性,將其替換爲id,因此改寫後結果不受影響,但顯然firstName和lastName是非分組列,可以通過MIN()和MAX()函數來繞過限制

      select min(a.first_name), max(a.last_name), ...
      
  • 如果沒有通過order by子句顯式指定排序列,當查詢使用group by子句的時候,結果集會自動按照分組的字段進行排序,如果不關心結果集的順序,而查詢又導致了filesort,可以使用order by null不進行排序,也可以在group by子句中直接使用desc或者asc設定方向

4.8.5 優化LIMIT分頁

在分頁的時候,如果偏移量非常大,比如 limit 10000, 20 ,這樣就需要查詢10020條記錄,但是隻返回20行數據,這樣的代價非常高

  • 一個思路是儘可能使用覆蓋索引,延遲關聯,假設有如下sql語句

    • select film_id, description from film order by titil limit 50, 5,如果表非常大,最好改寫成如下形式

      • select film_id, description 
        	from film 
        		inner join (
              select film_id from film
              order by title limit 50,5
            )as lim using(film_id)
        
      • 這裏的延遲關聯將大大提升查詢效率,利用覆蓋索引拿到主鍵,再根據主鍵查詢需要的行

  • 也可以將limit查詢轉換爲已知位置的查詢,讓MySQL通過範圍掃描索引獲得結果,例如下列sql語句,在列上有索引,預先計算了邊界值

  • select film_id, description
    	from film
    		where position between 50 and 54 order by position
    
  • 如果可以省去offset,也是可以進行優化的。每次翻頁都保存上次取數據的位置,下次翻頁就可以直接從該記錄的位置開始掃描,例如使用主鍵翻頁,需要主鍵是單調增長的

  • select * from a
    	order by id limit 6
    

    假設上次的查詢返回主鍵爲10-15的記錄,那麼下一頁就從15這個點開始

    select * from a
    	where id > 15
    		order by id limit 6
    

    這樣的性能是可以很好的

  • 最後一種做法是獲取並緩存較多的數據,例如緩存1000條,每次分頁都從緩存中獲取,如果結果集少於1000條,就顯示所有的分頁鏈接,如果大於1000條,大於的分頁鏈接隱藏起來,增加一個查找1000條以後數據的按鈕

    • 這個做法,是一次掃多行出來,然後受益於多次查詢的策略

4.8.6 優化UNION查詢

  • MySQL總是通過創建並填充臨時表的方式來執行UNION查詢。經常需要手工地將where、limit、order by等子句“下推”到UNION的各個子查詢中(上面也提到過了)
  • 除非有需要消除重複的行的需求,否則就一定要使用UNION ALL,如果沒有ALL關鍵字,MySQL會給臨時表加上DISTINCT選項,這會導致整個臨時表啊的數據做唯一性檢查,代價非常高

4.8.7 優化SELECT FOR UPDATE

要儘量避免使用SELECT FOR UPDATE,可以直接使用下面的變換替代SELECT FOR UPDATE

假設場景是使用MySQL構建一個任務隊列,表中每一條記錄都爲一項任務,等待客戶端取任務執行,每條任務都有3個狀態(state):

  • 0:待執行
  • 1:正在執行
  • 2:執行完畢

還有一列字段表示正在執行的線程名稱(thread_id,0爲無)

例如在一個事務中,先鎖住一條任務數據,然後將其狀態變爲正在運行狀態

begin;
-- 鎖住待執行的10個任務
select id from task
	where state = 0 and thread_id = 0
	limit 10 for update;
-- result: 1,2,3
-- 將這些任務更新爲正在執行
update task
	set state = 1 and thread_id = '12345'
	where id in(1,2,3);
commit;

改寫成下面的寫法將會更加高效

-- 將10個待執行的任務直接更新爲正在執行
update task
	set state = 1 and thread_id = '12345'
	where state = 0 and thread_id = 0
	limit 10;

-- 查詢那些剛剛鎖定的要執行的任務
select id from task
	where state = 1 and thread_id = '12345'

第一種寫法,鎖的時間是一整個事務,例子中即爲兩條語句的時間,但第二種寫法,只鎖了update那條語句的時間,但兩者效果都是一樣的,後者的併發性是更高的

所有的SELECT FOR UPDATE都可以使用類似的方法進行改寫

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章