查詢性能低下最根本的緣由是訪問的數據太多。某些查詢可能不可避免地須要篩選大量數據,但這並不常見。大部分性能低下的查詢均可以經過減小訪問的數據量進行優化。對於低效的查詢,能夠經過下面兩個步驟分析:mysql
有些查詢會請求超過實際須要的數據,而後這些多餘的數據會被應用程序丟棄。這會給MySQL服務器帶來額外的負擔,並增長網絡開銷,另外也會消耗應用服務器的CPU和內存資源。算法
典型案例:sql
對於MySQL,最簡單的衡量查詢開銷的三個指標:響應時間、掃描的行數和返回的行數。沒有哪一個指標可以完美地衡量查詢的開銷,但它們大體反映了MySQL在內部執行查詢時須要訪問多少數據,並能夠大概推算出查詢運行的時間。這三個指標都會記錄到MySQL的慢日誌中,檢查慢日誌記錄是找出掃描行數過多的查詢的好辦法。數據庫
不少高性能的應用都會對關聯查詢進行分解。簡單地,能夠對每個表進行一次單表查詢,而後將結果在應用程序中進行管理緩存
SELECT * FROM tag
JOIN tag_post ON tag_post.tag_id=tag.id
JOIN post ON tag_post.post_id=post.id
WHERE tag.tag='mysql';
-- 能夠分解成:
SELECT * FROM tag WHERE tag='mysql';
SELECT * FROM tag_post where tag_id=1234;
SELECT * FROM post whre post.id in (123, 456);複製代碼
當向MySQL發送一個請求的時候,MySQL的工做流程:性能優化
SHOW FULL PROCESSLIST
命令查看:
查詢的生命週期的下一步是將一個SQL轉換成一個執行計劃,MySQL再依照這個執行計劃和存儲引擎進行交互,這包括多個子階段:解析SQL、預處理、優化SQL執行計劃。這個過程當中的任何錯誤(例如語法錯誤)均可能終止查詢,另外在實際執行中,這幾部分可能一塊兒執行也可能單獨執行。服務器
語法解析器和預處理:網絡
查詢優化器:數據結構
通過語法解析器和預處理後,語法樹被認爲是合法的,並將由優化器將其轉化成執行計劃。架構
MySQL使用基於成本的優化器,它將嘗試預測一個查詢使用某種執行計劃時的成本,並選擇其中成本最小的一個。
SHOW STATUS LIKE 'Last_query_cost';
來查詢當前會話的當前查詢的成本,其值N爲MySQL的優化器認爲大概須要作N個數據頁的隨機查找才能完成當前的查詢。致使MySQL選擇錯誤的執行計劃的緣由:
優化策略:
MySQL可以處理的優化類型:
從新定義關聯表的順序:數據表的關聯並不老是按照在查詢中指定的順序執行。決定關聯的順序是優化器很重要的一部分功能。
將外鏈接轉換爲內鏈接:並非全部的OUTER JOIN語句都必須之外鏈接的方式執行。例如WHERE條件,庫表結構均可能會讓外鏈接等價於一個內鏈接。
使用等價變換規則:MySQL使用一些等價變換來簡化並規範表達式。它能夠合併和減小一些比較,還能夠移除一些恆成立和一些恆不成立的判斷。
優化COUNT()、MIN()和MAX():索引和列是否可爲空能夠幫助MySQL優化這類表達式。例如,要找到某一列的最小值,只須要查詢B-Tree索引最左端的記錄,MySQL能夠直接獲取,並在優化器生成執行計劃的時候就能夠利用這一點(優化器會將這個表達式做爲一個常數對待,在EXPLAIN就能夠看到"Select tables optimized away")。相似的,沒有任何WHERE條件的COUNT(*)查詢一般也可使用存儲引擎提供的一些優化(MyISAM維護了一個變量來存放數據表的行數)
預估並轉換爲常數表達式:MySQL檢測到一個表達式能夠轉換爲常數的時候,就會一直把該表達式做爲常數進行優化處理。例如:一個用戶自定義變量在查詢中沒有發生變化、數學表達式、某些特定的查詢(在索引列上執行MIN,甚至是主鍵或惟一鍵查找語句)、經過等式將常數值從一個表傳到另外一個表(經過WHERE、USING或ON來限制某列取值爲常數)。
覆蓋索引掃描:當索引中的列包含全部查詢中全部須要的列的時候,MySQL就可使用索引返回須要的數據,而無須查詢對應的數據行。
子查詢優化:在某些狀況下能夠將子查詢轉換成一種效率更高的形式,從而減小多個查詢屢次對數據的訪問。
提早終止查詢:當發現已經知足查詢的需求,可以馬上終止查詢。例如使用了LIMIT子句,或者發現一個不成立的條件(當即返回一個空結果)。當存儲引擎須要檢索」不一樣取值「或者判斷存在性的時候,例如DISTINCT,NOT EXIST()或者LEFT JOIN類型的查詢,MySQL都會使用這類優化。
等值傳播:若是兩個列的值經過等式關聯,那麼就能夠把其中一個列的WHERE條件傳遞到另外一個列上。
SELECT film.film_id
FROM sakila.film
INNER JOIN sakila.film_actor USING(film_id)
WHERE film.file_id > 500;
-- 若是手動經過一些條件來告知優化器這個WHERE條件適用於兩個表,在MySQL中反而讓查詢更難維護。
... WHERE film.file_id > 500 AND film_actor.film_id > 500;複製代碼
列表IN()的比較:不一樣於其它數據庫IN()徹底等價於多個OR條件語句,MySQL將IN()列表中的數據先進行排序,而後經過二分查找的方式來肯定列表的值是否知足條件,前者查詢複雜度爲O(n),後者爲O(log n)。對於有大量取值的狀況,MySQL這種處理速度會更快。
數據和索引的統計信息:
MySQL如何執行關聯查詢:
執行計劃:
關聯查詢優化器:
排序優化
不管如何排序都是一個成本很高的操做,因此從性能角度考慮,應儘量避免排序或者儘量避免對大量數據進行排序。
文件排序:當不能使用索引生成排序結果的時候,MySQL須要本身進行排序,若是數據量小則在內存中進行,若是數據量大則須要使用磁盤。
排序算法:
進行文件排序的時候須要使用的臨時存儲空間可能會比想象的要大得多。緣由在於MySQL排序時,對每個排序記錄都會分配一個足夠長的定長空間來存放。
在關聯查詢的時候排序:
若是ORDER BY子句中全部列都來自關聯的第一個表,那麼MySQL在關聯處理第一個表的時候進行文件排序。能夠在EXPLAIN看到Extra字段有「Using filesort」
除第一種場景,MySQL都會先將關聯的結果放到一個臨時表中,而後在全部的關聯都結束後,再進行文件排序操做。用EXPLAIN可看到「Using temporary;Using filesort」。若是查詢中有LIMIT的話,LIMIT也會在排序以後應用,因此即便須要返回較少的數據,臨時表和須要排序的數據仍然很是大。
5.6後版本在這裏作了些改進:當只須要返回部分排序結果的時候,例如使用了LIMIT子句,MySQL再也不對全部的結果進行排序,而是根據實際狀況,選擇拋棄不知足條件的結果,而後在進行排序。
在解析和優化階段,MySQL將生成查詢對應的執行計劃,MySQL的查詢執行引擎則根據這個執行計劃來完成整個查詢。
查詢執行階段不是那麼複雜,MySQL只是簡單地根據執行計劃給出的指令逐步執行。在根據執行計劃逐步執行的過程當中,又大量的操做須要調用存儲引擎實現的「handle API」接口來完成。
MySQL的萬能「嵌套循環」並非對每種查詢都是最優的,但只對少部分查詢不適用,咱們每每能夠經過改寫查詢讓MySQL高效地完成工做。另外,5.6版本會消除不少本來的限制,讓更多的查詢可以已儘量高的效率完成。
MySQL的子查詢實現得很是糟糕,最糟糕的一類查詢是WHERE條件語句中包含IN()的子查詢。
SELECT * FROM sakila.film
WHERE film_id IN(
SELECT film_id FROM sakil.film_actor WHERE actor_id =1 );
-- MySQL對IN()列表中的選項有專門的優化策略,但關聯子查詢並非這樣的,MySQL會將相關的外層表壓到子查詢中,它認爲這樣能夠高效地查找到數據行。也就是說,以上查詢會被MySQL更改爲:
SELECT * FROM sakila.film
WHERE EXISTS(
SELECT film_id FROM sakil.film_actor WHERE actor_id =1
AND film_actor.film_id = film.film_id);
-- 這時子查詢須要根據film_id來關聯外部表的film,由於須要film_id字段,因此MySQL認爲沒法先執行這個子查詢。經過EXPLIAN能夠看到子查詢是一個相關子查詢(DEPENDENT SUBQUERY),而且能夠看到對film表進行全表掃描,而後根據返回的film_id逐個進行子查詢。若是外層是一個很大的表,查詢性能會很糟糕。
-- 優化重寫方式1:
SELECT film.* FROM sakila.film
INNER JOIN sakil.film_actor USING(film_id)
WHERE actor_id =1;
-- 優化重寫方式2:使用函數GROUP_CONCAT()在IN()中構造一個逗號分割的列表。
-- 優化重寫方式3,使用EXISTS()等效的改寫查詢:
SELECT * FROM sakila.film
WHERE EXISTS(
SELECT film_id FROM sakil.film_actor WHERE actor_id =1
AND film_actor.film_id = film.film_id);複製代碼
有時,MySQL沒法將限制條件從外層「下推」到內層,這使得原表可以限制部分返回結果的條件沒法應用到內層查詢的優化上。
若是但願UNION的各個子句可以根據LIMIT只取部分結果集,或者但願可以先排好序再合併結果集的話,就須要在UNION的各個子句中分別使用這些子句。另外,從臨時表取出數據的順序是不必定的,若是要得到正確的順序,還須要加上一個全局的ORDER BY 和 LIMIT
(SELECT first_name, last_name
FROM sakila.actor
ORDER BY last_name)
UNION ALL
(SELECT first_name, last_name
FROM sakila.customer
ORDER BY last_name)
LIMIT 20;
-- 在UNION子句分別使用LIMIT
(SELECT first_name, last_name
FROM sakila.actor
ORDER BY last_name
LIMIT 20)
UNION ALL
(SELECT first_name, last_name
FROM sakila.customer
ORDER BY last_name
LIMIT 20)
LIMIT 20;複製代碼
MySQL並不支持鬆散索引掃描,也就沒法按照不連續的方式掃描一個索引。一般,MySQL的索引掃描須要先定義一個起點和終點,即便須要的數據只是這段索引中不多數的幾個,MySQL仍須要掃描這段索引中每個字段。
示例:假設咱們有索引(a,b),有如下查詢SELECT ... FROM tb1 WHERE b BETEWEEN 2 AND 3;
,由於只使用了字段b而不符合索引的最左前綴,MySQL沒法使用這個索引,從而只能經過全表掃描找到匹配的行。
瞭解索引結構的話,會發現還有一個更快的辦法執行上面的查詢。索引的物理結構(不是存儲引擎API)使得能夠先掃描a列第一個值對應的b列的範圍,而後在跳到a列第二個只掃描對應的b列的範圍,即鬆散索引掃描。這時就無須再使用WHERE過濾,由於已經跳過了全部不須要的記錄。MySQL並不支持鬆散索引掃描
MySQL5.0 之後的版本,某些特殊的場景下是可使用鬆散索引掃描的。例如,在一個分組查詢中須要找到分組的最大值和最小值:
-- 在Extra字段顯示「Using index for group-by」,表示使用鬆散索引掃描
EXPLAIN SELECT actor_id, MAX(film_id)
FROM sakila.film_actor
GROUP BY actor\G;複製代碼
在MySQL很好地支持鬆散索引掃描以前,一個簡單的繞過辦法就是給前面的列加上可能的常數值。5.6以後的版本,關於鬆散索引掃描的一些限制將會經過「索引條件下推(index condition pushdown)」的方式解決
對於MIN()和MAX()查詢,MySQL的優化作得並很差。
SELECT MIN(actor_id) FROM sakila.actor WHERE first_name = 'PENELOPE';
-- 由於在first_name上沒有索引,MySQL將會進行一次全表掃描。若是MySQL可以進行主鍵掃描,那麼理論上當MySQL讀到第一個知足條件的記錄,就是須要找到的最小值,由於主鍵是嚴格按照actor_id字段的大小順序排列的。
-- 曲線優化辦法:移除MIN(),而後使用LIMIT
SELECT actor_id FROM sakila.actor USE INDEX(PRIMARY) WHERE first_name = 'PENNLOPE' LIMIT 1;
-- 該SQL已經沒法表達它的本意,通常咱們經過SQL告訴服務器須要什麼數據,再由服務器決定如何最優地獲取數據。但有時候爲了得到更高的性能,須要放棄一些原則。複製代碼
MySQL不容許對同一張表同時進行查詢和更新。這其實並非優化器的限制,若是清楚MySQL是如何執行查詢的,就能夠避免這種狀況。能夠經過生成表來繞過該限制。
-- 符合標準的SQL,可是沒法運行
mysql> UPDATE tbl AS outer_tbl
-> SET cnt = (
-> SELECT count(*) FROM tbl AS inner_tbl
-> WHERE inner_tbl.type = outer_tbl.type
-> );
-- 生成表來繞過該限制:
mysql> UPDATE tbl
-> INNER JOIN(
-> SELECT type, count(*) AS cnt
-> FROM tbl
-> GROUP BY type
-> ) AS der USING(type)
-> SET tbl.cnt = der.cnt;複製代碼
若是對優化器選擇的執行計劃不滿意,可使用優化器提供的幾個提示(hint)來控制最終的執行計劃。不過MySQL升級後可能會致使這些提示無效,須要從新審查。
部分提示類型:
HIGH_PRIORITY和LOW_PRIORITY:
告訴MySQL當多個語句同時訪問某一個表的時候,這些語句的優先級。只對使用表鎖的存儲引擎有效,但即便是在MyISAM中也要慎重,由於這兩個提示會致使併發插入被禁用,可能會致使嚴重下降性能
DELAYED:
STRAIGHT_JOIN:
當MySQL沒能正確選擇關聯順序的時候,或者因爲可能的順序太多致使MySQL沒法評估全部的關聯順序的時候,STRAIGNT_JOIN都會頗有用。特別是在如下第二種狀況,MySQL可能會花費大量時間在」statistics「狀態,加上這個提示會大大減小優化器的搜索空間。
能夠先使用EXLPAN語句來查看優化器選擇的關聯順序,而後使用該提示來重寫查詢,肯定最優的關聯順序。可是在升級MySQL的時候,要從新審視這類查詢。
SQL_SMALL_RESULT和SQL_BIG_RESULT:
SQL_BUFFER_RESULT:
SQL_CACHE和SQL_NO_CACHE
SQL_CALC_FOUND_ROWS:
FOR UPDATE和LOCK IN SHARE MODE
USING INDEX、IGONRE INDEX和FORCE INDEX:
5.0和更新版本新增用來控制優化器行爲的參數:
count()的做用:
關於MyISAM的神話:
簡單的優化
利用MyISAM在count(*)全表很是快的特性,來加速一些特定條件的查詢。
-- 使用標準數據據worold
SELECT count(*) FROM world.city WHERE ID > 5;
-- 將條件反轉,可很大程度減小掃描行數到5行之內
SELECT (SELECT count(*) FROM world.city) - COUNT(*)
FROM world.city WHERE ID <= 5;複製代碼
示例:假設可能須要經過一個查詢返回各類不一樣顏色的商品數量
-- 使用SUM
SELECT SUM(IF(color = 'blue', 1, 0)) AS blue,SUM(IF(color = 'red', 1, 0)) AS red FROM items;
-- 使用COUNT,只須要將知足條件的設置爲真,不知足設置爲NULL
SELECT COUNT(color = 'blue' OR NULL) AS blue, COUNT(color = 'red' OR NULLASred FROM items;複製代碼
使用近似值:
更復雜的優化:
分頁的時候,另外一個經常使用的技巧是在LIMIT語句中加上SQL_CALC_FOUND_ROWS提示,這樣就能夠得到去掉LIMIT之後知足條件的行數,所以能夠做爲分頁的總數。加上這個提示後,MySQL無論是否須要都會掃描全部知足條件的行,而後拋棄掉不須要的行,而不是在知足LIMIT的行數後就終止掃描。因此該提示的代價可能很是高。
Percona Toolkit contains pt-query-advisor, a tool that parses a log of queries, analyzes
the query patterns, and gives annoyingly detailed advice about potentially bad practices
in them.
用戶自定義變量是一個用來存儲內容的臨時容器,在鏈接MySQL的整個過程當中都存在。在查詢中混合使用過程化和關係化邏輯的時候,該特性很是有用。
使用方法:
SET @one := 1;
SET @min_actor := (SELECT MIN(actor_id) FROM sakila.actor);
SET @last_week := CURRENT_DATE - INTERVAL 1 WEEK;
SELECT ... WHERE col <= @last_week;
-- 具備「左值」特性,在給一個變量賦值的同時使用這個變量
SELECT actor_id, @rownum := @rownum + 1 As rownum ...複製代碼
沒法使用的場景:
應用場景:
優化排名語句:
-- 查詢獲取演過最多電影的前10位演員,而後根據出演電影次數作一個排名,若是出演次數同樣,則排名相同。
mysql> SET @curr_cnt := 0, @prev_cnt := 0, @rank := 0;
-> SELECT actor_id,
-> @curr_cnt := cnt AS cnt,
-> @rank := IF(@prev_cnt <> @curr_cnt, @rank + 1, @rank) AS rank,
-> @prev_cnt := @curr_cnt AS dummy
-> FROM (
-> SELECT actor_id, COUNT(*) AS cnt
-> FROM sakila.film_actor
-> GROUP BY actor_id
-> ORDER BY cnt DESC
-> LIMIT 10
-> ) as der;複製代碼
避免重複查詢剛剛更新的數據:
-- 在更新行的同時又但願獲取獲得該行的信息。雖然看起來仍然須要兩個查詢和兩次網絡來回,但第二個查詢無須訪問任何數據表,速度會快不少
UPDATE t1 SET lastUpdated = NOW() WHERE id = 1 AND @now := NOW();
SELECT @now;複製代碼
統計更新和插入的數量
-- 使用了INSERT ON DUPLICATE KEY UPDATE的時候,想統計插入了多少行的數據,而且有多少數據是由於衝突而改寫成更新操做。
-- 實現該辦法的本質以下,當每次因爲衝突致使更新時對變量@x自增一次,而後經過對這個表達式乘以0來讓其不影響要更新的內容
INSERT INTO t1(c1, c2) VALUES(4, 4), (2, 1), (3, 1)
ON DUPLICATE KEY UPDATE
c1 = VALUES(c1) + ( 0 * ( @x := @x +1 ) );複製代碼
肯定取值的順序
一個最多見的問題,沒有注意到在賦值和讀取變量的使用多是在查詢的不一樣階段。
-- WHERE和SELECT是在查詢執行的不一樣階段被執行的,而WHERE是在ORDER BY文件排序操做以前執行。
mysql> SET @rownum := 0;
mysql> SELECT actor_id, @rownum := @rownum + 1 AS cnt
-> FROM sakila.actor
-> WHERE @rownum <= 1;
+----------+------+
| actor_id | cnt |
+----------+------+
| 1 | 1 |
| 2 | 2 |
+----------+------+複製代碼
儘可能讓變量的賦值和取值發生在執行查詢的同一個階段。
mysql> SET @rownum := 0;
mysql> SELECT actor_id, @rownum AS rownum
-> FROM sakila.actor
-> WHERE (@rownum := @rownum + 1) <= 1;複製代碼
將賦值運距放到LEAST(),這樣就能夠徹底不改變排序順序的時候完成賦值操做。這個技巧在不但願對子句的執行結果有影響卻又要完成變量複製的時候頗有用。這樣的函數還有GREATEST(), LENGTH(), ISNULL(), NULLIF(), IF(), 和COALESCE()。
-- LEAST()老是返回0
mysql> SET @rownum := 0;
mysql> SELECT actor_id, first_name, @rownum AS rownum
-> FROM sakila.actor
-> WHERE @rownum <= 1
-> ORDER BY first_name, LEAST(0, @rownum := @rownum + 1);複製代碼
編寫偷懶的UNION:
假設須要編寫一個UNION查詢,其第一個子查詢做爲分支條件先執行,若是找到了匹配的行,則跳過第二個分支。在某些業務場景中確實會有這樣的需求,好比如今一個頻繁訪問的表中查找「熱」數據,找不到再去另一個較少訪問的表中查找「冷數據「。(區分熱冷數據是一個很好提升緩存命中率的辦法)。
-- 在兩個地方查找一個用戶,一個主用戶表,一個長時間不活躍的用戶表,不活躍的用戶表的目的是爲了實現更高效的歸檔。
-- 舊的UNION查詢,即便在users表中已經找到了記錄,上面的查詢仍是會去歸檔表中再查找一次。
SELECT id FROM users WHERE id = 123
UNION ALL
SELECT id FROM users_archived WHERE id = 123;
-- 用一個偷懶的UINON查詢來抑制這樣的數據返回,當第一個表中沒有數據時,咱們纔在第二個表中查詢。一旦在第一個表中找到記錄,就定義一個變量@found,經過在結果列中作一次賦值來實現,而後將賦值放在函數GREATEST中來避免返回額外的數據。爲了明確結果來自哪個表,新增了一個包含表名的列。最後須要在查詢的末尾將變量重置爲NULL,保證遍歷時不干擾後面的結果。
SELECT GREATEST(@found := −1, id) AS id, 'users' AS which_tbl
FROM users WHERE id = 1
UNION ALL
SELECT id, 'users_archived'
FROM users_archived WHERE id = 1 AND @found IS NULL
UNION ALL
SELECT 1, 'reset' FROM DUAL WHERE ( @found := NULL ) IS NOT NULL;複製代碼
用戶自定義變量的其餘用處:
其餘用法:
使用MySQL來實現對列表是一個取巧的作法,不少系統在高流量、高併發的狀況下表現並很差。典型的模式是一個表包含多種類型的記錄:未處理記錄、已處理記錄、正在處理的記錄等等。一個或者多個消費者線程在表中查找未處理的記錄,而後聲稱正在處理,當處理完成後,再將記錄更新爲已處理狀態。通常的,例如郵件發送、多命令處理、評論修改等會使用相似模式,但
原有處理方式不合適的緣由:
優化過程:
將對列表分紅兩部分,即將已處理記錄歸檔或者存放到歷史表,這樣始終保證對列表很小。
找到未處理記錄通常來講都沒問題,若是有問題則能夠經過使用消息方式來通知各個消費者。
可已使用一個帶有註釋的SLEEP()函數作超時處理。這讓線程一直阻塞,直到超時或者另外一個線程使用KILL QUERY結束當前的SLEEP。所以,當再向對列表中新增一批數據後,能夠經過SHOW PROCESSLIST
,根據註釋找到當前正在休眠操做的線程,並將其KILL。可使用函數GET_LOCK和RELEASE_LOCK()來實現通知,或者能夠在數據庫以外實現,如使用一個消息服務。
SELECT /* waiting on unsent_emails */ SLEEP(10000), col1 FROM table;複製代碼
最後一個問題是如何讓消費者標記正在處理的記錄,而不至於讓多個消費者重複處理一個記錄。
儘可能避免使用SELECT FOR UPDATE,這一般是擴展性問題的根源,這會致使大量的書屋阻塞並等待。不光是隊列表,任何狀況下都要避免。
能夠直接使用UPDATE來更新記錄,而後檢查是否還有其餘的記錄須要處理。(全部的SELECT FOR UPDATE均可以使用相似的方式改寫)
-- 該表的owner用來存儲當前正在處理這個記錄的鏈接ID,即由函數CONNECTION_ID()返回額ID,若是當前記錄沒有被任何消費者處理,則該值爲0
CREATE TABLE unsent_emails (
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
-- columns for the message, from, to, subject, etc.
status ENUM('unsent', 'claimed', 'sent'),
owner INT UNSIGNED NOT NULL DEFAULT 0,
ts TIMESTAMP,
KEY (owner, status, ts)
);
-- 常見的處理辦法。這裏的SELECT查詢使用到索引的兩個列,理論上查找的效率應該更快。問題是,兩個查詢之間的「間隙時間」,這裏的鎖會讓全部其餘同一的查詢所有被阻塞。全部這樣的查詢將使用相同的索引,掃描索引相同結果的部分,因此極可能被阻塞。
BEGIN;
SELECT id FROM unsent_emails
WHERE owner = 0 AND status = 'unsent'
LIMIT 10 FOR UPDATE;
-- result: 123, 456, 789
UPDATE unsent_emails
SET status = 'claimed', owner = CONNECTION_ID()
WHERE id IN(123, 456, 789);
COMMIT;
-- 改進後更高效的寫法,無須使用SELECT查詢去找到哪些記錄尚未被處理。客戶端的協議會告訴你更新了幾條記錄,因此能夠直到此次須要處理多少條記錄。
SET AUTOCOMMIT = 1;
COMMIT;
UPDATE unsent_emails
SET status = 'claimed', owner = CONNECTION_ID()
WHERE owner = 0 AND status = 'unsent'
LIMIT 10;
SET AUTOCOMMIT = 0;
SELECT id FROM unsent_emails
WHERE owner = CONNECTION_ID() AND status = 'claimed';
-- result: 123, 456, 789複製代碼
最後還需處理一種特殊狀況:那些正在被進程處理,而進程自己卻因爲某種緣由退出的狀況。
只須要按期運行UPDATE語句將它都更新成原始狀態,而後執行SHOW PROCESSLIST,獲取當前正在工做的線程ID,並使用一些WHERE條件避免取到那些剛開始處理的進程
-- 假設獲取的線程ID有(十、20、30),下面的更新語句會將處理時間超過10分鐘的記錄狀態更新成初始狀態。
-- 將範圍條件放在WHERE條件的末尾,這個查詢剛好能勾使用索引的所有列,其它的查詢也都能使用上這個索引,這樣就避免了再新增一個額外的索引來知足其它的查詢
UPDATE unsent_emails
SET owner = 0, status = 'unsent'
WHERE owner NOT IN(0, 10, 20, 30) AND status = 'cla
AND ts < CURRENT_TIMESTAMP - INTERVAL 10 MINUTE;複製代碼
該案例中的一些基礎原則:
有時,最好的辦法就是將任務隊列從數據庫中遷移出來,Redis和memcached就是一個很好的隊列容器。
不建議使用MySQL作太複雜的空間計算存儲,PostgreSQL在這方面是一個不錯的選擇。一個典型的例子是計算以某個點爲中心,必定半徑內的全部點。例如查找某個點附近全部能夠出租的房子,或者社交網站中」匹配「附近的用戶。
假設咱們有以下表,這裏經度和緯度的單位都是度:
CREATE TABLE locations (
id INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(30),
lat FLOAT NOT NULL,
lon FLOAT NOT NULL
);
INSERT INTO locations(name, lat, lon)
VALUES('Charlottesville, Virginia', 38.03, −78.48),
('Chicago, Illinois', 41.85, −87.65),
('Washington, DC', 38.89, −77.04);複製代碼
假設地球是圓的,而後使用兩點所在最大圓(半正矢)公式來計算兩點之間的距離。現有座標latA和lonA、latB和lonB,那麼點A和點B的距離計算公式以下:
ACOS(
COS(latA) * COS(latB) * COS(lonA - lonB)
+ SIN(latA) * SIN(latB)
)複製代碼
計算的結果是一個弧度,若是要將結果轉換成英里或公里,則須要乘以地球的半徑。
SELECT * FROM locations WHERE 3979 * ACOS(
COS(RADIANS(lat)) * COS(RADIANS(38.03)) * COS(RADIANS(lon) - RADIANS(-78.48))
+ SIN(RADIANS(lat)) * SIN(RADIANS(38.03))
) <= 100;複製代碼
這類查詢不只沒法使用索引,並且還會很是消耗CPU時間,給服務器帶來很大的壓力,並且還得反覆計算。
優化地方:
看看是否真的須要這麼精確的計算。其實該算法已經有不少不精確的地方:
若是不須要過高的精度,能夠認爲地球是圓的。要想有更多的優化,能夠將三角函數的計算放到應用中,而不要在數據庫中計算。
看看是否真須要計算一個圓周,能夠考慮直接使用一個正方形代替。邊長爲200英里的正方形,一個頂點到中心的距離大概是141英里,這和實際計算的100英里相差並不太遠。根據正方形公式來計算弧度爲0.0253(100英里)的中心到邊長的距離:
SELECT * FROM locations
WHERE lat BETWEEN 38.03 - DEGREES(0.0253) AND 38.03 + DEGREES(0.0253)
AND lon BETWEEN −78.48 - DEGREES(0.0253) AND −78.48 + DEGREES(0.0253);複製代碼
如今看看如何用索引來優化這個查詢:
新增兩個列,用來存儲座標的近似值FLOOR(),而後在查詢中使用IN()將全部點的整數值都放到列表中:
mysql> ALTER TABLE locations
-> ADD lat_floor INT NOT NULL DEFAULT 0,
-> ADD lon_floor INT NOT NULL DEFAULT 0,
-> ADD KEY(lat_floor, lon_floor);複製代碼
如今能夠根據座標的必定範圍的近似值來搜索,這個近似值包括地板值和天花板值,地理上分別對應的是南北:
-- 查詢某個範圍的全部點,數值須要在應用程序中計算而不是MySQL
mysql> SELECT FLOOR( 38.03 - DEGREES(0.0253)) AS lat_lb,
-> CEILING( 38.03 + DEGREES(0.0253)) AS lat_ub,
-> FLOOR(-78.48 - DEGREES(0.0253)) AS lon_lb,
-> CEILING(-78.48 + DEGREES(0.0253)) AS lon_ub;
+--------+--------+--------+--------+
| lat_lb | lat_ub | lon_lb | lon_ub |
+--------+--------+--------+--------+
| 36 | 40 | −80 | −77 |
+--------+--------+--------+--------+
-- 生成IN()列表中的整數:
SELECT * FROM locations
WHERE lat BETWEEN 38.03 - DEGREES(0.0253) AND 38.03 + DEGREES(0.0253)
AND lon BETWEEN −78.48 - DEGREES(0.0253) AND −78.48 + DEGREES(0.0253)
AND lat_floor IN(36,37,38,39,40) AND lon_floor IN(-80,-79,-78,-77);複製代碼
使用近似值會讓咱們的計算結果有誤差,因此咱們還須要一些額外的條件過濾在正方形以外的點,這和前面使用CRC32作哈希索引相似:先建一個索引過濾出近似值,在使用精確條件匹配全部的記錄並移除不知足條件的記錄。
事實上,到這時就無須根據正方形的近似來過濾數據,可使用最大圓公式或者畢達哥拉斯定理來計算:
SELECT * FROM locations
WHERE lat_floor IN(36,37,38,39,40) AND lon_floor IN(-80,-79,-78,-77)
AND 3979 * ACOS(
COS(RADIANS(lat)) * COS(RADIANS(38.03)) * COS(RADIANS(lon) - RADIANS(-78.48))
+ SIN(RADIANS(lat)) * SIN(RADIANS(38.03))
) <= 100;複製代碼
這時計算精度再次回到使用一個精確的圓周,不過如今的作法更快。只要可以高效地過濾掉大部分的點,例如使用近似整數和索引,以後再作精確數學計算的代價並不大。只要不是使用大圓周的算法,不然速度會更慢。
該案例使用的優化策略:
若是把建立高性能應用程序比做是一個環環相扣的」難題「,除了前面介紹的schema、索引和查詢語句設計以外,查詢優化應該是解開」難題「的最後一步。
理解查詢是如何被執行的以及時間都消耗在哪些地方,這依然是前面介紹的響應時間的一部分。再加上一些諸如解析和優化過程的知識,就能夠額更進一步地理解上一章討論的MySQL如何訪問表和索引的內容了。這也從另外一個維度理解MySQL在訪問表和索引時查詢和索引的關係。
優化一般須要三管齊下:不作、少作、快速地作。