MySQL DBA必讀:萬字歸總表設計與SQL編寫技巧

前言

隨着移動雲平臺系統業務不斷增長,必然需要對各系統進行更新或者發佈新版本,以滿足用戶的需求。但是在系統更新或者發佈新版本時,往往會引起各種的性能隱患,使得系統的穩定性大大降低,這些性能隱患的根本原因大部分都在於軟件開發標準不規範引起。爲了加強開發標準規範,提高各系統的穩定性,本文從影響MySQL數據庫性能的常見因素入手,重點論述下如何編寫高效的SQL語句,並以合適的方式創建表和索引,以達到系統在不斷更新和升級時仍能保持良好的穩定性。

在這之前,首先簡單瞭解一下MySQL的體系結構。

MySQL的邏輯結構

Connectors:用來與客戶端應用程序建立連接的數據庫接口。

Management Services & Utilities:系統管理和服務控制相關的輔助工具。

Connection Pool:負責處理與用戶訪問有關的各種用戶登錄、線程處理、內存和進程緩存需求。

Sql Interface:提供從用戶接受命令並把結果返回給用戶的機制。

Parser:對SQL語句進行語法分析和解析,構造一個月來執行查詢的數據結構。

Optimizer:優化查詢語句,以保證數據檢索動作的效率達到或者非常接近最最優。使用一種“選取-投影-聯結”策略來處理查詢,即先根據有關的限制條件進行選取(Select 操作)以減少將要處理的元組個數,再進行投影以減少被選取元組力的屬性字段的個數,最後根據連接條件生產最終的查詢結果。

Caches & Buffers:保證使用頻率最高的數據或結構能夠以最有效率的方式被訪問,緩存的類型有:表緩存、記錄緩存、鍵緩存、權限緩存、主機名緩存等。

Query流程

1、查詢緩存

檢查查詢緩存是否打開,檢查是否命中緩存中的數據(通過對大小寫敏感的HASH查找實現的),若不命中則進行下一階段的處理。若命中查詢緩存,檢查用戶權限,若權限沒問題,則直接把緩存數據返回給客戶端。

2、語法解析器和預處理器

詞法/語法解析器:將會進行語法規則的驗證和解析查詢(對語法解析),生成語法分析樹。

預處理器:根據MySQL規則進一步檢查語法分析樹是否合法。例如檢查表或列是否存在,解析名字和別名有沒有歧義。下一步預處理器會驗證權限。

3、查詢優化器

優化器的作用就是找到最好的執行計劃。MySQL使用CBO優化器。MySQL使用很多優化策略生成最優的執行計劃,可以分爲兩類:靜態優化(編譯時優化)、動態優化(運行時優化)。

4、查詢執行引擎

MySQL只是簡單的根據執行計劃給出的指令逐步執行。調用存儲引擎實現的接口來完成執行計劃。優化器根據接口可以獲取表的相關信息,包括表的所有列名、索引統計信息等。將結果返回給客戶端,或者返回這個查詢的一些信息,如查詢影響到的行數。如果查詢可以被緩存,那麼MySQL會將結果存放到查詢緩存中。

影響MySQL數據庫的常見因素

1、服務器硬件

CPU:一般情況下CPU資源不會是性能瓶頸的直接原因;MySQL不支持多cpu對同一SQL併發處理。

內存:直接影響MySQL緩衝池的大小及MySQL數據庫的整體運行穩定性;如內存資源不足,容易造成MySQL的會話擁堵甚至實例重啓。

存儲IO:直接影響MySQL的處理性能;在大量數據變更的業務場景下,對存儲的IO性能要求往往較高。

2、數據庫存儲引擎

MyISAM:不支持事務型查詢,在OLTP類型業務場景中不建議使用。

InnoDB:支持事務型查詢,支持行級鎖,對併發業務支持較好。

3、MySQL參數

1)連接請求的參數:max_connections

MySQL的最大連接數,增加該值增加mysqld要求的文件描述符的數量。連接請求量大時,建議調高此值調的越高,內存開銷越大。

mysql>show variables like 'max_connections';
+-------------------------+----------+
|Variable_name|Value|
+-------------------------+----------+
|max_connections|512 |
+-------------------------+----------+
mysql>show status like 'max%connections';
+---------------------------+----------+
|Variable_name|Value|
+---------------------------+----------+
|max_used_connections|512 |
+---------------------------+----------+

2)全局緩存參數

key_buffer_size指定索引緩衝區的大小,它決定索引處理的速度,尤其是索引讀的速度。Key_reads是內存中沒有找到索引直接從硬盤讀取索引的數量。

mysql>show variables like' key_buffer_size'; 
+-------------------------+-------------+
|Variable_name|Value|
+-------------------------+-------------+
|key_buffer_size|536870912 |
+-------------------------+-------------+
mysql>show status like 'key_read%'; 
+-------------------------+---------------+
|Variable_name|Value|
+-------------------------+---------------+
|Key_read_requests|178306331520 |
|Key_reads|67 |
 +-------------------------+---------------+

使用查詢緩衝,MySQL將查詢結果存放在緩衝區中,今後對於同樣的SELECT語句(區分大小寫),將直接從緩衝區中讀取結果。

mysql>show variables like ' key_buffer_size';
mysql>show status like ' key_read%';
查詢緩存碎片率= Qcache_free_blocks/ Qcache_total_blocks* 100%
查詢緩存利用率= (query_cache_size–Qcache_free_memory) / query_cache_size* 100%
查詢緩存命中率= (Qcache_hits–Qcache_inserts) / Qcache_hits* 100%

3)每個連接的緩存參數

① Sort_buffer_size

每個需要進行排序的線程分配該大小的一個緩衝區。增加這值加速ORDER BY或GROUP BY操作。默認數值是2097144(2M),可改爲16777208 (16M)。

② Join_buffer_size

聯合查詢操作所能使用的緩衝區大小。

record_buffer_size,read_rnd_buffer_size,sort_buffer_size,join_buffer_size爲每個線程獨佔,也就是說,如果有100個線程連接,則佔用爲16M*100。

③ table_open_cache

表高速緩存的大小。每當MySQL訪問一個表時,如果在表緩衝區中還有空間,該表就被打開並放入其中,這樣可以更快地訪問表內容。

mysql> show global status like 'open%tables%';
+-----------------+-------+
| Variable_name| Value |
+-----------------+-------+
| Open_tables| 1024 |
| Opened_tables| 1465 |
+-----------------+-------+
mysql>showvariableslike'table_open_cache';
+----------------------+-------+
|Variable_name|Value|
+----------------------+-------+
|table_open_cache|1024|
+----------------------+-------+

④ tmp_table_size

臨時表大小。通過設置tmp_table_size選項來增加一張臨時表的大小,例如做高級GROUP BY操作生成的臨時表。

mysql>showglobal statuslike' created_tmp%';
+-----------------------------+----------+
|Variable_name|Value |
+-----------------------------+----------+
|Created_tmp_disk_tables|21197|
| Created_tmp_files| 58|
| Created_tmp_tables| 1771587 |
+-----------------------------+----------+
mysql> show variables like 'tmp_table_size';
+-----------------+------------+
| Variable_name| Value |
+-----------------+------------+
| tmp_table_size| 16777216 |
+-----------------+------------+

⑤ thread_cache_size

可以複用的保存在緩衝區中的線程的數量。當客戶端斷開之後,服務器處理此客戶的線程將會緩存起來以響應下一個客戶而不是銷燬(前提是緩存數未達上限)。

mysql>show global status like 'Thread%';
+----------------------+-------+
|Variable_name|Value|
+----------------------+-------+
|Threads_cached|31|
|Threads_connected|239|
|Threads_created|2914|
|Threads_running|4|
+----------------------+-------+
mysql>show variables like 'thread_cache_size';
+---------------------+-------+
|Variable_name|Value|
+---------------------+-------+
|thread_cache_size|32|
+---------------------+-------+

4)配置InnoDB的參數

① Innodb_buffer_pool_size

InnoDB使用該參數指定大小的內存來緩衝數據和索引,其對InnoDB的重要性等於key_buffer_size對MyISAM的重要性。

② Innodb_log_buffer_size

Innodb_log緩存大小,一般爲1-8M,默認爲1M,對於較大的事務,可以增大緩存大小。可設置爲4M或8M。

5)慢查詢參數:log_slow_queries

4、數據庫表設計

表體量過大:字段過多或者記錄數過多的“大表”,在查詢中會消耗大量資源,且執行效率低;建議根據業務類型拆分大表(分區表)

使用外鍵:無論是MySQL還是Oracle,都不建議採用外鍵進行表關聯。

缺少主鍵:無論對於主從同步還是查詢性能,主鍵發揮的作用都非常重要;建議所有業務表都添加主鍵。

5、SQL語句

多表關聯:多表關聯容易造成關聯數據過大,影響查詢效率;建議查詢中的關聯表數量不超過2個。

全表掃描:觸發全表掃描容易造成大量IO讀寫,嚴重降低查詢效率;建議在查詢條件中加入帶索引的過濾條件。

根據現網環境優化執行的難易度,在優化順序可以按照:SQL語句->數據庫表設計->數據庫參數配置->數據庫存儲引擎->服務器硬件

下文我們重點論述上面第四、第五點,通過編寫高效的SQL語句,並以合適的方式創建表和索引,使系統始終保持良好的性能。

表設計建議

以合適的方式建立表,可以提高數據庫運行效率,有效降低歷史數據清理時的維護工作難度。

1、選定存儲引擎

MySQL支持多種存儲引擎,在處理不同類型的應用時,可以通過選擇使用不同的存儲引擎提高應用的效率,或者提供靈活的存儲。MySQL的存儲引擎包括:MyISAM、 InnoDB、BDB、MEMORY、MERGE、EXAMPLE、NDB Cluster、ARCHIVE、CSV、BLACKHOLE、FEDERATED等。下面是幾種常用的存儲引擎的對比和推薦使用方式。

其中,InnoDB 存儲引擎提供了具有提交、回滾和崩潰恢復能力的事務安全。其設計目的主要面向在線事務處理(OLTP)及應用。但是對比 Myisam的存儲引擎,InnoDB 寫的處理效率差一些並且會佔用更多的磁盤空間以保留數據和索引。從MySQL5.5版本開始,InnoDB存儲引擎是默認的存儲引擎。Myisam存儲引擎不支持事務,表鎖設計,支持羣文索引,主要面向一些OLAP數據庫應用及Web應用。每個MyISAM在磁盤上存儲成三個文件。文件名都和表名相同,擴展名分別是.frm(存儲表定義)、.MYD (MYData,存儲數據)、.MYI (MYIndex,存儲索引)。數據文件和索引文件可以放置在不同的目錄,平均分佈IO,獲得更快的速度。在移動雲生產環境中我們建議所有業務表必須是innodb表。

2、表命名規範

1)命名大小寫規範:在 MySQL 中,數據庫對應數據目錄中的目錄。數據庫中的每個表至少對應數據庫目錄中的一個文件(也可能是多個,取決於存儲引擎)。因此,所使用操作系統的大小寫敏感性決定了數據庫名和表名的大小寫敏感性。這說明在大多數 Unix 中數據庫名和表名對大小寫敏感,而在 Windows 中對大小寫不敏感。MySQL有配置參數lower_case_table_names,不可動態更改,linux系統默認爲 0,即庫表名以實際情況存儲,大小寫敏感。如果是1,以小寫存儲,大小寫不敏感。如果是2,以實際情況存儲,但以小寫比較。MySQL5.6默認爲0。若大小寫混合使用,易導致使用及管理混亂,且字段名顯式區分大小寫,但實際使用不區分,即不可以建立兩個名字一樣但大小寫不一樣的字段。因此,建議爲了統一規範, 庫名、表名、字段名使用小寫字母,連接統一用下劃線‘_’。

2)命名字符長度規範:庫名、表名、字段名支持最多64個字符,但爲了統一規範、易於辨識以及減少傳輸量,禁止超過32個字符。

3)避免使用MySQL保留字:當庫名、表名、字段名等屬性含有保留字時,SQL語句必須用反引號引用屬性名稱,這將使得SQL語句書寫、SHELL腳本中變量的轉義等變得非常複雜。

3、建立常規表

MySQL常規表對應到文件系統上單個數據文件。在MySQL5.6中建表時,不指定任何參數,默認會建立存儲引擎爲innodb的常規表。常規表使用與大部分應用場景。默認情況下,由於部分操作系統對文件大小的限制,表大小限制爲2G。

4、建立分區表

MySQL從5.1版本開始支持分區表,從5.6開始MySQL表分區以單個數據文件形式存儲於文件系統中,根據所使用的不同分區規則可以分成幾大類型:

RANGE 分區:基於屬於一個給定連續區間的列值,把多行分配給分區。比較常用如按照時間字段劃分分區,2019年1月的數據放到201901分區,2019年2月的數據放到201902分區以此類推。範圍分區方式適用於應用中頻繁對分區鍵值進行範圍查詢的場合。另外針對部監控表隨時間不斷累積數據,大量的歷史數據積壓,一方面會降低應用程序的效率,另一方面亦浪費大量的存儲空間。因此需要對歷史表進行定期清理,以基本保持當前總數據量。基於這個原則,建議對所有歷史表按清理時間鍵值進行範圍分區,時間範圍建議按月進行。表分區的命名採用以下的規範:<表名>_pYYYYMMDD,其中YYYY爲分區數據的年份,MM爲分區數據的月份,DD爲分區數據的日期。

LIST 分區:類似於按RANGE分區,區別在於LIST分區是基於列值匹配一個離散值集合中的某個值來進行選擇。列值分區與範圍分區有類似之處,該分區與範圍分區類似的是需要指定列的值,但是其分區值必須明確指定。

HASH分區:基於用戶定義的表達式的返回值來進行選擇的分區,該表達式使用將要插入到表中的這些行的列值進行計算。這個函數可以包含MySQL中有效的、產生非負整數值的任何表達式。此種分區方式最適用於查詢條件中,對分區字段進行單值查詢的情況(如,col=1)。但是hash分區,並不適用於對索引字段使用範圍查詢,如對字段使用大於>,小於<,操作的查詢語句中。

KEY分區:類似於按HASH分區,區別在於KEY分區只支持計算一列或多列,且MySQL服務器提供其自身的哈希函數。必須有一列或多列包含整數值。

複合分區:基於RANGE/LIST 類型的分區表中每個分區的再次分割。子分區可以是 HASH/KEY 等類型。

5、表字段規範

  • 儘量使用TINYINT、SMALLINT、MEDIUM_INT作爲整數類型而非INT,如果非負則加上UNSIGNED;
  • VARCHAR的長度只分配真正需要的空間;
  • 使用枚舉或整數代替字符串類型;
  • 儘量使用TIMESTAMP而非DATETIME;
  • 單表不要有太多字段,建議在20以內;
  • 避免使用NULL字段,很難查詢優化且佔用額外索引空間;
  • 用整型來存IP。

6、統一字符集

系統、服務端、客戶端、庫、表、開發程序端需統一字符集,通常中英文環境用utf8

表使用建議

根據MySQL的表建立規範,以及在實際維護中的表使用經驗相結合,對錶使用作出如下的建議。

1、選擇合適的數據類型

InnoDB 存儲引擎和數據列。建議使用 varchar類型:對於InnoDB數據表,內部的行存儲格式沒有區分固定長度和可變長度列(所有數據行都使用指向數據列值的頭指針),因此在本質上,使用固定長度的 char列不一定比使用可變長度varchar列簡單。因而,主要的性能因素是數據行使用的存儲總量。由於CHAR平均佔用的空間多於varchar,因此使用varchar來最小化需要處理的數據行的存儲總量和磁盤I/O是比較好的。

2、text和blob

在使用text和blob字段類型時要注意以下幾點,以便更好的發揮數據庫的性能:

1)text和blob值在執行了大量的刪除或更新操作的時候容易影響效率。

刪除該類型值會在數據表中留下很大的"空洞",以後填入這些"空洞"的記錄可能長度不同,爲了提高性能,建議定期使用 OPTIMIZE TABLE 功能對這類表進行碎片整理。

2)使用合成的(synthetic)索引。

合成的索引列在某些時候是有用的。一種辦法是根據其它的列的內容建立一個散列值,並把這個值存儲在單獨的數據列中。之後可以通過檢索散列值找到數據。但是,這種索引只能用於精確匹配的查詢(散列值對於類似<或>=等範圍搜索操作符 是沒有用處的)。可以使用MD5()函數生成散列值,也可以使用SHA1()或CRC32(),或者使用自己的應用程序邏輯來計算散列值。需注意數值型散列值可以很高效率地存儲。同樣,如果散列算法生成的字符串帶有尾部空格,此時不要把它們存儲在char與varchar列中,它們會受到尾部空格去除的影響。合成的散列索引對於那些text和blob數據列特別有用。用散列標識符值查找的速度比搜索blob列本身的速度快很多。

3)把text或blob列分離到單獨的表中。

通過把這些數據列移動到單獨的數據表中,可以讓你把原數據表中的數據列轉換爲固定長度的數據行格式。這會減少主表中的碎片,使你得到固定長度數據行的性能優勢。此時能避免在主數據表上運行 SELECT *查詢的時候通過網絡傳輸大量的text或blob值。

3、拆分大字段、訪問頻率低的字段

將大字段、訪問頻率低的字段拆分到單獨的表中存儲,分離冷熱數據。有利於有效利用緩存,防止讀入無用的冷數據,較少磁盤IO,同時保證熱數據常駐內存提高緩存命中率。

4、數據文件磁盤分離

MySQL表以數據文件形式存儲於文件系統,針對不同的表的讀寫會打開不同的數據文件。建議對不同的熱表進行存儲的磁盤分離。通過將不同的熱表建立在不同的lun上,分散I/O,這樣就能進一步減少I/O消耗的瓶頸。

索引建立規範

建立合適的索引,是提高數據庫運行效率的一個很好的工具,這種效果是立竿見影的,但這裏也不並不是說表上的索引越多越好,過之而不及。在數據庫設計過程中,需要爲表選擇一些合適的索引。在數據庫中索引的維護代價是表的3倍,寧缺勿濫,這是建立索引時的一個遵循標準。

索引使用規範

根據MySQL的索引使用經驗相結合,對索引使用做出如下的建議。

1、根據表數據量評估索引

詳細評估和分析建立索引所在表的實際數據量,數據量達到GB級別、記錄數達到百萬級別、訪問頻繁的表,需要建立合適的索引。相反,在數據量較少且訪問頻率不高的情況下,如只有一百行記錄以下的表不需要建立索引。因爲在數據量少的情況下,使用全表掃描效果比走索引更好。

2、選擇適當的索引字段

索引字段的選擇需要結合業務需求,評估出應用中作爲查詢條件出現比較頻繁的字段,在此字段上建立單獨或者複合索引。選擇建立索引的字段,應該遵循以下的原則:

1)高選擇性,選擇性是指通過索引字段查詢返回結果集佔表總數據量的百分比,結果集佔表總數據量的百分比越小選擇性越高,反之越低。選擇性越高,通過索引查詢返回的結果集越少,索引更爲高效。在OLTP應用系統中,選擇性應高於1,也就是結果集佔表總數據量的百分比應<1%。

2)空值少,避免在空值(Null)很多的字段上建立B-tree索引,大量空值會降低索引效率,索引字段中的空值佔總數據量的百分比應少於10%。

3)數據分佈均勻,索引字段中,個別數據值佔總數據量的百分率明顯比其它數據值佔總數據量的百分率高,表明該字段數據值分佈不均,容易引起數據庫選擇錯誤索引,生成錯誤的查詢執行計劃。應該避免在數據值分佈不均的字段上建立索引。

3、避免過度索引

每個額外的索引都要佔用額外的磁盤空間,並降低寫操作的性能,這一點我們前面已經介紹過。在修改表的內容時,索引必須進行更新,有時可能需要重構,因此,索引越多,所花的時間越長。如果有一個索引很少利用或從不使用,那麼會不必要地減緩表的修改速度。此外,MySQL在生成一個執行計劃時,要考慮各個索引,這也要費時間。創建多餘的索引給查詢優化帶來了更多的工作。索引太多,也可能會使 MySQL 選擇不到所要使用的最好索引。只保持所需的索引有利於查詢優化。如果想給已索引的表增加索引,應該考慮所要增加的索引是否是現有多列索引的最左索引。如果是,則就不要費力去增加這個索引了,因爲已經有了。

4、使用唯一索引

考慮某列中值的分佈。對於唯一值的列,索引的效果最好,而具有多個重複值的列,其索引效果最差。例如,存放年齡的列具有不同值,很容易區分各行。而用來記錄性別的列,只含有“ 男”和“女”,則對此列進行索引沒有多大用處(不管搜索哪個值,都會得出大約一半的行)。

5、使用短索引

如果對串列進行索引,應該指定一個前綴長度,只要有可能就應該這樣做。例如,如果有一個 CHAR(200) 列,如果在前10個或20個字符內,多數值是惟一的,那麼就不要對整個列進行索引。對前10個或20個字符進行索引能夠節省大量索引空間,也可能會使查詢更快。較小的索引涉及的磁盤 I/O 較少,較短的值比較起來更快。更爲重要的是,對於較短的鍵值,索引高速緩存中的塊能容納更多的鍵值,因此,MySQL也可以在內存中容納更多的值。這增加了找到行而不用讀取索引中較多塊的可能性。

6、利用符合索引前置列

在創建一個 n 列的索引時,實際是創建了 MySQL 可利用的 n 個索引。多列索引可起幾個索引的作用,因爲可利用索引中最左邊的列集來匹配行。這樣的列集稱爲最左前綴。(這與索引一個列的前綴不同,索引一個列的前綴是利用該的前 n 個字符作爲索引值。) 例如:(a,b,c)、(a,b),後者爲冗餘索引。當SQL的where條件包含a,b時,能正確的走前一索引,後者作爲冗餘沒有建立的必要。關鍵在於找到適合的前置列,可以避免建冗餘的索引。

7、考慮在列上進行的比較類型

索引可用於“<”、“<=”、“=”、“>=”、“>”和 BETWEEN 運算。在模式具有一個直接量前綴時,索引也用於 LIKE 運算。如果只將某個列用於其他類型的運算時(如 STRCMP( )),對其進行索引沒有價值。

高效SQL編寫規範建議

1、大批量插入數據

**

**

如果同時執行大量的插入,建議使用多個值的INSERT語句(方法二)。這比使用分開INSERT語句快(方法一),一般情況下批量插入效率有幾倍的差別。

方法一:

insert into tablename values(1,2); 
insert into tablename values(1,3); 
insert into tablename values(1,4);

方法二:

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

選擇後一種方法的原因有二。

  • 減少SQL語句解析的操作, MySQL沒有類似Oracle的share pool,採用方法二,只需要解析一次就能進行數據的插入操作;
  • SQL語句較短,可以減少網絡傳輸的IO。

此外,還有以下建議提高插入性能:

  • 通過使用 INSERT DELAYED 語句得到更高的速度。Delayed 的含義是讓 insert 語句馬上執行,其實數據都被放在內存的隊列中,並沒有真正寫入磁盤;
  • 這比每條語句分別插入要快的多,但需要注意,DELAYED關鍵字只用於MyISAM,MEMORY這類只支持表鎖的存儲引擎;
  • 將索引文件和數據文件分在不同的磁盤上存放(利用建表中的選項)。

2、查詢優先還是更新(insert、update、delete)優先

MySQL 還允許改變語句調度的優先級,它可以使來自多個客戶端的查詢更好地協作,這樣單個客戶端就不會由於鎖定而等待很長時間。改變優先級還可以確保特定類型的查詢被處理得更快。我們首先應該確定應用的類型,判斷應用是以查詢爲主還是以更新爲主的,是確保查詢效率還是確保更新的效率,決定是查詢優先還是更新優先。下面我們提到的改變調度策略的方法主要是針對只存在表鎖的存儲引擎,比如 MyISAM 、MEMROY、MERGE,對於Innodb 存儲引擎,語句的執行是由獲得行鎖的順序決定的。MySQL 的默認的調度策略可用總結如下:

1)寫入操作優先於讀取操作。

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

3)對某張數據表的多個讀取操作可以同時地進行。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語句的影響。

3、避免出現select *

select * 操作在任何類型數據庫中都不是一個好的SQL開發習慣。使用select * 取出全部列,會讓優化器無法完成索引覆蓋掃描這類優化,會影響優化器對執行計劃的選擇,也會增加網絡帶寬消耗,更會帶來額外的I/O,內存和CPU消耗。建議評估業務實際需要的列數,指定列名以取代select *。

  • 規範:Select col1,col2,col3… from t1;
  • 不規範:Select * from t1。

4、避免使用insert…selec…語句

當使用insert…select…進行記錄的插入時,如果select的表是innodb類型的,不論insert的表是什麼類型的表,都會對select的表的紀錄進行鎖定。對於那些從Oracle遷移過來的應用,需要特別的注意,因爲Oracle並不存在類似的問題,所以在Oracle的應用中insert…select…操作非常常見。例如:有時候會對比較多的紀錄進行統計分析,然後將統計的中間結果插入到另外一個表,這樣的操作因爲進行的非常少,所以可能並沒有設置相應的索引。

如果遷移到MySQL數據庫後不進行相應的調整,那麼在進行這個操作期間,對需要select的表實際上是進行的全表掃描導致的所有記錄的鎖定,將會對應用的其他操作造成非常嚴重的影響。

究其主要原因,是因爲MySQL在實現複製的機制時和Oracle是不同的,如果不進行select表的鎖定,則可能造成從數據庫在恢復期間插入結果集的不同,造成主從數據的不一致。如果不採用主從複製,關閉binlog並不能避免對select紀錄的鎖定。如果使用這個binlog進行從數據庫的恢復,或者進行主數據庫的災難恢復,都將可能和主數據庫的執行效果不同。

因此,我們並不推薦通過設置這個參數來避免insert…select…導致的鎖,如果需要進行可能會掃描大量數據的insert…select操作,我們推薦使用select…into outfile和load data infile的組合來實現,這樣是不會對紀錄進行鎖定的。

例子:

INSERT INTO SMAP_HISTORY.SMAP2_SESSION (SESSION_ID,SESSION_TICKET_ID) SELECT S.SESSION_ID,S.SESSION_TICKET_ID FROM SMAP.SMAP2_SESSION S WHERE SESSION_SID = #sessionId#;

以上語句會對錶SMAP2_SESSION施加表鎖,而由於業務上該表存在大量insert語句,業務壓力大的時候極易造成嚴重的阻塞。

5、適當使用commit

適當使用commit可以釋放事務佔用的資源而減少消耗,commit後能釋放的資源如下:

  • 事務佔用的undo數據塊;
  • 事務在redo log中記錄的數據塊;
  • 釋放事務施加的,減少鎖爭用影響性能。特別是在需要使用delete刪除大量數據的時候,必須分解刪除量並定期commit。

6、減少表的鎖衝突

對 Innodb 類型的表:

1)首先要確認,在對錶獲取行鎖的時候,要儘量的使用索引檢索紀錄,如果沒有使用索引訪問,那麼即便你只是要更新其中的一行紀錄,也是全表鎖定的。要確保 sql 是使用索引來訪問紀錄的,必要的時候,請使用 explain 檢查 sql 的執行計劃,判斷是否按照預期使用了索引。

2)由於 MySQL 的行鎖是針對索引加的鎖,不是針對紀錄加的鎖,所以雖然是訪問不同行的紀錄,但是如果是相同的索引鍵,是會被加鎖的。應用設計的時候也要注意,這裏和 Oracle 有比較大的不同。

3)當表有多個索引的時候,不同的事務可以使用不同的索引鎖定不同的行,當表有主鍵或者唯一索引的時候,不是必須使用主鍵或者唯一索引鎖定紀錄,其他普通索引同樣可以用來檢索紀錄,並只鎖定符合條件的行。

4)如果要使用鎖定讀,(SELECT … FOR UPDATE 或 … LOCK IN SHARE MODE),嘗試用更低的隔離級別,比如 READ COMMITTED。

7、使用SQL_BUFFER_RESULT減少鎖定時間

將強制 MySQL 生成一個臨時結果集。只要所有臨時結果集生成後,所有表上的鎖定均被釋放。這能在遇到表鎖定問題時或要花很長時間將結果傳給客戶端時有所幫助。當處理一個會讓客戶端耗費點時間才能處理的大結果集時,可以考慮使用SQL_BUFFER_RESULT 提示字。這樣可以告訴MySQL將結果集保存在一個臨時表中,這樣可以儘早的釋放各種鎖。需注意,該參數不能用於子查詢中以及union之後 語法:SELECT SQL_BUFFER_RESULT …

8、正確使用hint優化語句

MySQL中可以使用hint指定優化器在執行時選擇或忽略特定的索引。一般而言,處於版本變更帶來的表結構索引變化,更建議避免使用hint,而是通過Analyze table多收集統計信息。但在特定場合下,指定hint可以排除其他索引干擾而指定更優的執行計劃。

1)USE INDEX 在你查詢語句中表名的後面,添加 USE INDEX 來提供希望 MySQL 去參考的索引列表,就可以讓 MySQL 不再考慮其他可用的索引。例子: SELECT col1 FROM table USE INDEX (mod_time, name)…

2)IGNORE INDEX 如果只是單純的想讓 MySQL 忽略一個或者多個索引,可以使用 IGNORE INDEX 作爲 Hint。例子: SELECT col1 FROM table IGNORE INDEX (priority) …

3)FORCE INDEX 爲強制 MySQL 使用一個特定的索引,可在查詢中使用FORCE INDEX 作爲Hint。例子: SELECT col1 FROM table FORCE INDEX (mod_time) …

9、優化group by語句

默認情況下,MySQL 排序所有 “GROUP BY col1,col2,…;” 查詢的方法如同在查詢中指定 “ORDER BY col1,col2,…;” 如果顯式包括一個包含相同的列的 ORDER BY子句,MySQL 可以毫不減速地對它進行優化,儘管仍然進行排序。

如果查詢包括 GROUP BY 但你想要避免排序結果的消耗,你可以指定 ORDER BY NULL禁止排序。例如:

SELECT a, COUNT(1) FROM table GROUP BY a ORDER BY NULL ;

10、優化order by語句

在某些情況中,MySQL 可以使用一個索引來滿足 ORDER BY 子句,而不需要額外的排序。where 條件和 order by 使用相同的索引,並且 order by 的順序和索引順序相同 ,並且 order by 的字段都是升序或者都是降序。

例如:下列 SQL 可以使用索引。

SELECT col1 FROM t1 ORDER BY key_part1,key_part2,... ; 
SELECT col1 FROM t1 WHERE key_part1=1 ORDER BY key_part1 DESC, key_part2 DESC; SELECT col1 FROM t1 ORDER BY key_part1 DESC, key_part2 DESC;

以上覆合索引包含字段key_part1,key_part2…

但是以下情況不使用索引:

SELECT col1 FROM t1 ORDER BY key_part1 DESC, key_part2 ASC; 

以上由於order by 的字段混合 ASC和 DESC 。

SELECT col1 FROM t1 WHERE key2=constant ORDER BY key1; 

以上用於查詢行的關鍵字與 ORDER BY 中所使用的不相同。

SELECT col1 FROM t1 ORDER BY key1, key2; 

對不同的索引關鍵字使用 ORDER BY:

11、優化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 不需要在內存中創建臨時表來完成這個邏輯上的需要兩個步驟的查詢工作。

12、優化or條件

對於 or 子句,如果要利用索引,則or 之間的每個條件列都必須用到索引;如果沒有索引,則應該考慮增加索引。

13、優化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';

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

  • 簡單的SQL容易使用到MySQL的QUERY CACHE;
  • 減少鎖表時間特別是使用MyISAM存儲引擎的表;
  • 可以使用多核CPU。

15、使用truncate代替delete

當刪除全表中記錄時,使用delete語句的操作會被記錄到undo塊中,刪除記錄也記錄binlog,當確認需要刪除全表時,會產生很大量的binlog並佔用大量的undo數據塊,此時既沒有很好的效率也佔用了大量的資源。使用truncate替代,不會記錄可恢復的信息,數據不能被恢復。也因此使用truncate操作有其極少的資源佔用與極快的時間。另外,使用truncate可以回收表的水位。

16、使用合理的分頁方式以提高分頁效率

使用合理的分頁方式以提高分頁效率 針對展現等分頁需求,合適的分頁方式能夠提高分頁的效率。

案例1:

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

上述例子通過一次性根據過濾條件取出所有字段進行排序返回。數據訪問開銷=索引IO+索引全部記錄結果對應的表數據IO。因此,該種寫法越翻到後面執行效率越差,時間越長,尤其表數據量很大的時候。

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

案例2:

select t.* from ( 
select id from t 
where thread_id = 10000 and deleted = 0 order by gmt_create asc limit 0, 15) a, t 
where a.id = t.id; 

上述例子必須滿足t表主鍵是id列,且有覆蓋索引secondary key:(thread_id, deleted, gmt_create)。通過先根據過濾條件利用覆蓋索引取出主鍵id進行排序,再進行join操作取出其他字段。數據訪問開銷=索引IO+索引分頁後結果(例子中是15行)對應的表數據IO。因此,該寫法每次翻頁消耗的資源和時間都基本相同,就像翻第一頁一樣。

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

17、避免不走索引的各種場景

在下面的SQL語句中的WHERE子句不使用索引:

1)條件中有or,且or左右列並非全部由索引 Select col1 from table where key1=1 or no_key=2

2)like查詢以%開頭

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

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

索引包含key_part1,key_part2,key_part3三列,但SQL語句沒有包含索引前置列。

4)隱式類型轉換造成不使用索引

Select col1 from table where key_varchar=123; 

上述語句由於索引對列類型爲varchar,但給定的值爲數值,涉及隱式類型轉換,造成不能正確走索引。

5)避免對索引字段進行計算

避免對索引字段進行任何計算操作,對索引字段的計劃操作會讓索引的作用失效,令數據庫選擇其他的較爲低效率的訪問路徑。

6)避免對索引字段進行是否NULL值判斷

避免使用索引列值是否可爲空的索引,如果索引列值可以是空值,在SQL語句中那些要返回NULL值的操作,將不會用到索引。

7)避免對索引字段不等於符號

使用索引列作爲條件進行查詢時,需要避免使用<>或者!=等判斷條件。如確實業務需要,使用到不等於符號,需要在重新評估索引建立,避免在此字段上建立索引,改由查詢條件中其他索引字段代替。

18、避免重複查詢更新的數據

針對業務中經常出現的更新行同時又希望獲得改行信息的需求,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表數據量較大時,後者比前者快很多。

19、避免出現不確定結果的函數

特定針對主從複製這類業務場景。由於原理上從庫複製的是主庫執行的語句,使用如now()、rand()、sysdate()、current_user()等不確定結果的函數很容易導致主庫與從庫相應的數據不一致。另外不確定值的函數,產生的SQL語句無法利用QUERY CACHE。

使用EXPLAIN分析SQL性能

1、執行計劃

執行計劃是一條查詢語句在數據庫中的執行過程或訪問路徑的描述。

2、怎樣查看MySQL執行計劃

在需要查看執行計劃的SQL前面添加explain並執行,即可獲取。

3、讀EXPLAIN中的信息

1)table

顯示這一行的數據是關於哪張表的。

2)type

這是重要的列,顯示連接使用了何種類型。

從最好到最差的連接類型爲const、eq_reg、ref、range、index和ALL。

3)possible_keys

顯示可能應用在這張表中的索引。如果爲空,沒有可能的索引。

4)key

實際使用的索引。如果爲NULL,則沒有使用索引。很少的情況下,MYSQL會選擇優化不足的索引。

5)key_len

使用的索引的長度。在不損失精確性的情況下,長度越短越好。

6)ref

顯示索引的哪一列被使用了,如果可能的話,是一個常數。

7)rows

MYSQL認爲必須檢查的用來返回請求數據的行數。

8)Extra

關於MYSQL如何解析查詢的額外信息。效率最低的是Using temporary和Using filesort,意味着MYSQL根本不能使用索引,所以檢索會很慢。

4、使用EXPLAIN中分析SQL性能

例1:

看左邊sql語句的執行計劃,看得出沒有走任何索引,屬於全表掃描,導致執行時間比較長。通過給業務表添加適當的索引,這條語句的執行時間由9秒變爲60毫秒,掃描行數由257910行變成1行,效率提升明顯。

例2:

原語句執行的時候是全表掃面,分頁查詢效率低,改爲利用主鍵來獲取後,執行時間由33秒變爲1.2秒,效率提升明顯。

慢日誌查詢管理平臺

爲了提高SQL分析和優化的效率,我們開始利用自動化的方式來實現慢SQL的獲取和智能分析,平臺頁面如下:

這個是我們現在在做的,慢日誌查詢和智能分析平臺,可以很方便的獲取慢查詢語句,並通過預先設置的優化規則,自動得到一些優化建議。

上述功能並不是完全是自主開發的,而是藉助了開源工具yearningSQL並做了一些擴展,也就是加入我們自定義的一些規則。然後,平臺對要執行的SQL做分析。通過觸碰事先定義好的規則來判斷這個SQL是否可以通過審覈,無法通過自動審覈的SQL再由人工來處理。

總結

本文從MySQL的邏輯原理和影響MySQL數據庫性能的常見因素入手,着重討論瞭如何以合適的方式創建表和索引以及編寫高效的SQL語句,並通過EXPLAIN對SQL的性能進行分析,最後給出了初步的自動化解決方案,希望能夠對大家有所幫助。

作者介紹

劉書浩,“移動雲”DBA,負責“移動雲”業務系統的數據庫運維、標準化等工作;擅長MySQL技術領域,熟悉MySQL複製結構、Cluster架構及運維優化;具有自動化運維經驗,負責“移動雲”數據庫管理平臺的搭建。

本文由 dbaplus 社羣授權轉載。

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