當MySQL單表記錄數過大時,增加、刪除、修改和查詢的性能將急劇下降,您可以參考以下步驟進行優化。
單表優化
除非你預計未來你的單表數據會不斷的持續上漲,否則不要一開始就考慮做拆分,拆分將增加邏輯、部署、操作和維護上的各種復雜性。一般以整數值為主的表在千萬級以下,以字符串為主的表在五百萬以下不是什么大問題。
字段
- 盡量使用TINYINT、SMALLINT、MEDIUM_INT作為整數類型而非INT,如果非負則加上UNSIGNED
- VARCHAR的長度只分配真正需要的空間
- 使用枚舉或整數代替字符串類型
- 盡量使用TIMESTAMP而非DATETIME,
- 單表不要有太多字段,建議在20以內
- 避免使用NULL字段,很難查詢優化且占用額外索引空間
- 用整型來存IP
索引
- 索引并不是越多越好,要根據查詢有針對性的創建,考慮在WHERE和ORDER BY命令上涉及的列建立索引,可根據EXPLAIN來查看是否用了索引還是全表掃描
- 盡量避免對where子句中的字段進行空值判斷,否則引擎將放棄索引并掃描整個表
- 具有稀疏值分布的字段不適合索引,例如只有兩個值的“性別”
- 字符字段只建前綴索引
- 字符字段最好不要是主鍵
- 不用使用數據庫自帶的約束外鍵,由程序邏輯層面實現約束
- 使用多列索引時主意順序和查詢條件保持一致,刪除不必要的單列索引
查詢SQL
- 可通過開啟慢查詢日志來找出較慢的SQL
- 不做列運算:SELECT id WHERE age + 1 = 10,任何對列的操作都將導致表掃描,它包括數據庫教程函數、計算表達式等等,查詢時要盡可能將操作移至等號右邊
- sql語句盡可能簡單:一條sql只能在一個cpu運算;大語句拆小語句,減少鎖時間;一條大sql可以堵死整個庫
- 不用`SELECT *``
- OR改寫成IN:OR的效率是n級別,IN的效率是log(n)級別,in的個數建議控制在200以內
- 不用函數和觸發器,在應用程序實現
- 避免%xxx式查詢
- 少用JOIN
- 使用同類型進行比較,比如用'123'和'123'比,123和123比
- 盡量避免在WHERE子句中使用!=或<>操作符,否則將引擎放棄使用索引而進行全表掃描
- 對于連續數值,使用BETWEEN不用IN:SELECT id FROM t WHERE num BETWEEN 1 AND 5
- 列表數據不要拿全表,要使用LIMIT來分頁,每頁數量也不要太大
引擎
目前被廣泛使用的引擎是MyISAM和InnoDB:
MyISAM
MyISAM引擎是MySQL 5.1及之前版本的默認引擎,它的特點是:
- 不支持行鎖,讀取時對需要讀到的所有表加鎖,寫入時則對表加排它鎖
- 不支持事務
- 不支持外鍵
- 不支持崩潰后的安全恢復
- 在表有讀取查詢的同時,支持往表中插入新紀錄
- 支持BLOB和TEXT的前500個字符索引,支持全文索引
- 支持延遲更新索引,極大提升寫入性能
- 對于不會進行修改的表,支持壓縮表,極大減少磁盤空間占用
InnoDB
- InnoDB在MySQL 5.5后成為默認索引,它的特點是:
- 支持行鎖,采用MVCC來支持高并發
- 支持事務
- 支持外鍵
- 支持崩潰后的安全恢復
- 不支持全文索引 總體來講,MyISAM適合SELECT密集型的表,而InnoDB適合INSERT和UPDATE密集型的表
系統調優參數
可以使用下面幾個工具來做基準測試:
- sysbench:一個模塊化,跨平臺以及多線程的性能測試工具
- iibench-mysql:基于 JAVA 的 MySQL/Percona/MariaDB 索引進行插入性能測試工具
- tpcc-mysql:Percona開發的TPC-C測試工具
具體的調優參數內容較多,詳情請參閱官方文檔,以下是一些常見重要的參數:
- back_log:back_log值指出在MySQL暫時停止回答新請求之前的短時間內多少個請求可以被存在堆棧中。也就是說,如果MySql的連接數據達到max_connections時,新來的請求將會被存在堆棧中,以等待某一連接釋放資源,該堆棧的數量即back_log,如果等待連接的數量超過back_log,將不被授予連接資源。可以從默認的50升至500
- wait_timeout:數據庫連接閑置時間,閑置連接會占用內存資源。可以從默認的8小時減到半小時
- max_user_connection: 最大連接數,默認為0無上限,最好設一個合理上限
- thread_concurrency:并發線程數,設為CPU核數的兩倍
- skip_name_resolve:禁止對外部連接進行DNS解析,消除DNS解析時間,但需要所有遠程主機用IP訪問
- key_buffer_size:索引塊的緩存大小,增加會提升索引處理速度,對MyISAM表性能影響最大。對于內存4G左右,可設為256M或384M,通過查詢show status like 'key_read%',保證key_reads / key_read_requests在0.1%以下最好
- innodb_buffer_pool_size:緩存數據塊和索引塊,對InnoDB表性能影響最大。通過查詢show status like 'Innodb_buffer_pool_read%',保證(Innodb_buffer_pool_read_requests – Innodb_buffer_pool_reads) / Innodb_buffer_pool_read_requests越高越好
- innodb_additional_mem_pool_size:InnoDB存儲引擎用來存放數據字典信息以及一些內部數據結構的內存空間大小,當數據庫對象非常多的時候,適當調整該參數的大小以確保所有數據都能存放在內存中提高訪問效率,當過小的時候,MySQL會記錄Warning信息到數據庫的錯誤日志中,這時就需要該調整這個參數大小
- innodb_log_buffer_size:InnoDB存儲引擎的事務日志所使用的緩沖區,一般來說不建議超過32MB
- query_cache_size:緩存MySQL中的ResultSet,也就是一條SQL語句執行的結果集,所以僅僅只能針對select語句。當某個表的數據有任何任何變化,都會導致所有引用了該表的select語句在Query Cache中的緩存數據失效。所以,當我們的數據變化非常頻繁的情況下,使用Query Cache可能會得不償失。根據命中率(Qcache_hits/(Qcache_hits+Qcache_inserts)*100))進行調整,一般不建議太大,256MB可能已經差不多了,大型的配置型靜態數據可適當調大. 可以通過命令show status like 'Qcache_%'查看目前系統Query catch使用大小
- read_buffer_size:MySql讀入緩沖區大小。對表進行順序掃描的請求將分配一個讀入緩沖區,MySql會為它分配一段內存緩沖區。如果對表的順序掃描請求非常頻繁,可以通過增加該變量值以及內存緩沖區大小提高其性能
- sort_buffer_size:MySql執行排序使用的緩沖大小。如果想要增加ORDER BY的速度,首先看是否可以讓MySQL使用索引而不是額外的排序階段。如果不能,可以嘗試增加sort_buffer_size變量的大小
- read_rnd_buffer_size:MySql的隨機讀緩沖區大小。當按任意順序讀取行時(例如,按照排序順序),將分配一個隨機讀緩存區。進行排序查詢時,MySql會首先掃描一遍該緩沖,以避免磁盤搜索,提高查詢速度,如果需要排序大量數據,可適當調高該值。但MySql會為每個客戶連接發放該緩沖空間,所以應盡量適當設置該值,以避免內存開銷過大。
- record_buffer:每個進行一個順序掃描的線程為其掃描的每張表分配這個大小的一個緩沖區。如果你做很多順序掃描,可能想要增加該值
- thread_cache_size:保存當前沒有與連接關聯但是準備為后面新的連接服務的線程,可以快速響應連接的線程請求而無需創建新的
- table_cache:類似于thread_cache_size,但用來緩存表文件,對InnoDB效果不大,主要用于MyISAM
升級硬件
Scale up,更不用說,根據MySQL是CPU密集型還是I/O密集型,可以通過提升CPU、內存和SSD都能顯著提高MySQL的性能。
讀寫分離
這也是目前常用的優化方法。一般來說,從庫讀主庫寫,一般不要采用雙主或多主引入很多復雜性,嘗試使用本文中的其他方案來提高性能。同時,許多當前的拆分解決方案也考慮了讀寫分離。
緩存
緩存可以發生在這些層次:
- MySQL內部:在系統調優參數介紹了相關設置
- 數據訪問層:比如MyBatis針對SQL語句做緩存,而Hibernate可以精確到單個記錄,這里緩存的對象主要是持久化對象Persistence Object
- 應用服務層:這里可以通過編程手段對緩存做到更精準的控制和更多的實現策略,這里緩存的對象是數據傳輸對象Data Transfer Object
- Web層:針對web頁面做緩存
- 瀏覽器客戶端:用戶端的緩存
可以根據實際情況在一個層次或多個層次結合加入緩存。這里重點介紹下服務層的緩存實現,目前主要有兩種方式:
- 直寫式(Write Through):在數據寫入數據庫后,同時更新緩存,維持數據庫與緩存的一致性。這也是當前大多數應用緩存框架如Spring Cache的工作方式。這種實現非常簡單,同步好,但效率一般。
- 回寫式(Write Back):當有數據要寫入數據庫時,只會更新緩存,然后異步批量的將緩存數據同步到數據庫上。這種實現比較復雜,需要較多的應用邏輯,同時可能會產生數據庫與緩存的不同步,但效率非常高。
表分區
MySQL版本5.1中引入的分區是一個簡單的水平拆分。用戶在構建表時需要添加分區參數,這對應用程序是透明的,而無需修改代碼。
對于用戶來說,分區表是一個獨立的邏輯表,但是底層由多個物理子表組成。實現分區的代碼實際上是通過一組底層表對象封裝的,但是對于SQL層來說,它是一個完全封裝底層的黑盒。MySQL實現分區的方式還意味著索引是根據分區的子表定義的,并且沒有全局索引。
用戶的SQL語句是需要針對分區表做優化,SQL條件中要帶上分區條件的列,從而使查詢定位到少量的分區上,否則就會掃描全部分區,可以通過EXPLAIN PARTITIONS來查看某條SQL語句會落在那些分區上,從而進行SQL優化,如下圖5條記錄落在兩個分區上:
mysql> explain partitions select count(1) from user_partition where id in (1,2,3,4,5);
+----+-------------+----------------+------------+-------+---------------+---------+---------+------+------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+----------------+------------+-------+---------------+---------+---------+------+------+--------------------------+
| 1 | SIMPLE | user_partition | p1,p4 | range | PRIMARY | PRIMARY | 8 | NULL | 5 | Using where; Using index |
+----+-------------+----------------+------------+-------+---------------+---------+---------+------+------+--------------------------+
1row in set (0.00 sec)
分區的好處是:
- 可以讓單表存儲更多的數據
- 分區表的數據更容易維護,可以通過清楚整個分區批量刪除大量數據,也可以增加新的分區來支持新插入的數據。另外,還可以對一個獨立分區進行優化、檢查、修復等操作
- 部分查詢能夠從查詢條件確定只落在少數分區上,速度會很快
- 分區表的數據還可以分布在不同的物理設備上,從而搞笑利用多個硬件設備
- 可以使用分區表賴避免某些特殊瓶頸,例如InnoDB單個索引的互斥訪問、ext3文件系統的inode鎖競爭
- 可以備份和恢復單個分區
分區的限制和缺點:
- 一個表最多只能有1024個分區
- 如果分區字段中有主鍵或者唯一索引的列,那么所有主鍵列和唯一索引列都必須包含進來
- 分區表無法使用外鍵約束
- NULL值會使分區過濾無效
- 所有分區必須使用相同的存儲引擎
分區的類型:
- RANGE分區:基于屬于一個給定連續區間的列值,把多行分配給分區
- LIST分區:類似于按RANGE分區,區別在于LIST分區是基于列值匹配一個離散值集合中的某個值來進行選擇
- HASH分區:基于用戶定義的表達式的返回值來進行選擇的分區,該表達式使用將要插入到表中的這些行的列值進行計算。這個函數可以包含MySQL中有效的、產生非負整數值的任何表達式
- KEY分區:類似于按HASH分區,區別在于KEY分區只支持計算一列或多列,且MySQL服務器提供其自身的哈希函數。必須有一列或多列包含整數值
分區適合的場景有:
- 最適合的場景數據的時間序列性比較強,則可以按時間來分區,如下所示:
CREATE TABLE members (
firstname VARCHAR(25) NOT NULL,
lastname VARCHAR(25) NOT NULL,
username VARCHAR(16) NOT NULL,
email VARCHAR(35),
joined DATE NOT NULL
)
PARTITION BY RANGE( YEAR(joined) )(
PARTITION p0 VALUES LESS THAN (1960),
PARTITION p1 VALUES LESS THAN (1970),
PARTITION p2 VALUES LESS THAN (1980),
PARTITION p3 VALUES LESS THAN (1990),
PARTITION p4 VALUES LESS THAN MAXVALUE
);
查詢時加上時間范圍條件效率會非常高,同時對于不需要的歷史數據能很容的批量刪除。
- 如果數據有明顯的熱點,而且除了這部分數據,其他數據很少被訪問到,那么可以將熱點數據單獨放在一個分區,讓這個分區的數據能夠有機會都緩存在內存中,查詢時只訪問一個很小的分區表,能夠有效使用索引和緩存
另外MySQL有一種早期的簡單的分區實現 - 合并表(merge table),限制較多且缺乏優化,不建議使用,應該用新的分區機制來替代