千萬級資料下的Mysql優化
前言
平時在寫一些小web系統時,我們總會對mysql不以為然。然而真正的系統易用應該講資料量展望拓展到千萬級別來考慮。因此,今天下午實在是無聊的慌,自己隨手搭建一個千萬級的資料庫,然後對資料庫進行一些簡單的CRUD來看看大資料情況下的CRUD效率。
結果發現,曾經簡單的操作,在資料量大的時候還是會造成操作效率低下的。因此先寫下這篇文章,日後不斷更新紀錄一下自己工作學習到的Mysql優化技巧。
搭建千萬級資料庫
首先,需要一個測試環境。一開始想到的是寫一個SImple JDBC程式然進行簡單的資料INSERT。結果發現單執行緒情況下,每次INSERT了一百多萬條的時候效率就變得非常的低下。但是程式也沒報OUT MEMORY之類的異常。初步判斷應該是單一執行緒不斷的瘋狂建立PrepareStatement物件CG沒來得及清理造成記憶體逐漸被吃緊的原因。
後來改進了一下,用多執行緒的機制。建立十個程序,每個負責一萬條資料的插入。這效率一下子提升了好幾倍。然而好景不長,很快的Java程式報錯:OUT MEMORY。記憶體溢位了,CG沒來得及清理。
這可把我給急的了。插入的太快記憶體CPU吃緊,插入的太慢又失去了建立測試環境“快”的初衷。後來想了下,既然是要批量插入資料,那麼不是可以簡單的寫一段資料庫儲存過程嗎?
於是,先建立一張測試表,就叫goods表吧。先寫sql語句建立表:
CREATE TABLE goods (
id serial,
NAME VARCHAR (10),
price DOUBLE
) ENGINE = MYISAM DEFAULT CHARACTER
SET = utf8 COLLATE = utf8_general_ci AUTO_INCREMENT = 1 ROW_FORMAT = COMPACT;
接下來根據表結構寫一段儲存過程讓資料庫自行重複插入資料:
begin
declare i int default 0 ;
dd:loop
insert into goods values
(null,'商品1',20),
(null,'商品2',18),
(null,'商品3',16),
(null,'商品4',4),
(null,'商品5',13),
(null,'商品6',1),
(null,'商品7',11),
( null,'商品8',12),
(null,'商品9',13),
(null,'商品0',12);
commit;
set i = i+10 ;
if i = 10000000 then leave dd;
end if;
end loop dd ;
end
寫完後執行一下,ok千萬級別的資料庫馬上就插入進去了。
再談資料庫優化
既然有了資料現在開始進入資料庫優化環節。
一、分頁查詢的優化
首先我們常常涉及到的CRUD操作莫過於分頁操作。 對於普通的分頁操作我們常常是這樣子
select *from goods limit 100,1000;
這樣當然沒有任何的問題,但是當我們的資料量非常大,假如我要檢視的是第八百萬條資料呢?對應的sql語句為:
select *from goods limit 8000000,1000;
Mysql執行時間為 1.5秒左右。那麼我們可以做一些什麼優化嗎?
上述的sql語句造成的效率低下原因不外乎:
大的分頁偏移量會增加使用的資料,MySQL會將大量最終不會使用的資料載入到記憶體中。就算我們假設大部分網站的使用者只訪問前幾頁資料,但少量的大的分頁偏移量的請求也會對整個系統造成危害
那麼我要怎樣來優化呢?如果我們的id為自增的。也就是說每一條記錄的id為上一條id + 1那麼,分頁查詢我們可以使用id進行範圍查詢替代傳統的limit。
例如上述的sql語句可以代替為:
select *from goods where id > 8000000 limit 1000;
上述sql的到同樣的執行結果,執行時間卻只有0.04秒。提升了40倍左右。
結論:對應於自增id的表,如果資料量非常大的分頁查詢,可以觀察id的分佈規律計算出其id的範圍通過範圍查詢來實現分頁效果。
二、索引優化
談到資料庫效率,大部分人的第一想法應該就是建立索引。沒錯,正確的建立索引可以很好的提升效率。
關於索引,這是一個很大的話題我就不打算在這篇文章概括起來了。推薦一篇美團技術部落格關於索引的文章。這篇文章很好的概述了索引的使用場景。主要要注意最左字首匹配原則,並且將索引建立在區分度高的列。區分度的計算公式為:
count(distinct col)/count(*)
因此像我的模擬資料中即使建立了索引效率也提升不了多少,因為區分度非常的低。
總結一些索引會失效的情況,我們在實際的開發中應該儘量避免:
- like查詢是以%開頭,不會使用索引
- WHERE條件中有or,即使其中有條件帶索引也不會使用
- !=,not in ,not exist不會使用索引
- WHERE字句的查詢條件裡使用了函式或計算
- 複合索引如果單獨使用,只有複合索引裡第一個欄位有效
結論:索引很重要,也是一個大話題。推薦看看那篇美團技術部落格的文章可以學習到很多。有些看似簡單的函式操作如果放在SQL語句中卻會導致索引失效,嚴重的影響效率,因此推薦將一些操作放到客戶端中進行計算而不是SQL語句中。索引的使用情況可以使用EXPLAIN進行檢視。
三、談談COUNT(*)
查詢一張表有多少條記錄常用語句為:
SELECT COUNT(*) FROM `goods`;
有些人認為這裡使用了星號可能效率不如直接使用COUNT(COL)來的高,所以他們認為對於goods表(存在邏輯主鍵)更高效的語句應該是這樣的:
SELECT COUNT(id) FROM `goods`;
但是其實兩條執行的時間是一樣的。因為COUNT(*)預設走最短的索引。由於id是這裡最短的索引所以COUNT(*)等價於COUNT(id)。
結論:如果表中的最短索引很長,而且需要COUNT(*)操作,不放新增一個冗餘的索引在一個比較短的列上,這樣可以大大加大索引的速度。並且記住:COUNT(*)走的永遠是最優的。
四、varchar 不是越大越好
有人認為varchar在的大小是按資料實際大小儲存的,所以為了防止長度溢位就一開始就將長度定義的很長。但是事實是:
- VARCHAR在硬碟佔用上確實是按實際大小佔用
- 但如果涉及到臨時表,是按後面的數字分配記憶體的
- 在VARCHAR列建立索引,ken_len也是按照後面數字分配的
結論:varchar按需取長,防止臨時表佔滿記憶體溢位至磁碟導致速度下降。
五、聯合查詢與單表查詢的選擇
Mysql有很多聯合查詢的方式,諸如left join、inner join等等。
但是這些聯合查詢其實效率是很低的,現在考慮兩張表一張為job表 資料量大約10萬 + ,另外一張是job的分析表資料量較少,想通過job表中的job_name查詢所有工作的工作分析情況。其中在兩張表的列 job_name 均建立了索引。現在如果用聯合查詢:
SELECT * FROM `job` a LEFT JOIN `job_analysis` b ON a.job_name = b.job_name;
-- 執行時間: 0.93s
-- EXPLAIN 結果:
1 SIMPLE a ALL 169497
1 SIMPLE b ref job_name job_name 212 jobs.a.job_name 1
可以看到使用left join的查詢只有一張表使用了索引,而另外一張表卻要ALL去遍歷。這對資料不是很大的時候還好,對資料量上百萬 千萬簡直是噩夢。
誠然這種情況下使用單表多次效率並不能更高(至少一次ALL + 一次走索引)但資料量大還是要選擇單表多次可能更優,因為單表多次查詢有利於後面對資料的分庫分表,且多次查詢可以支援部分的快取操作以及分為多次減少資料庫鎖的競爭。
摘自《高效能MYSQL》
事實上,用分解關聯查詢的方式重構查詢有如下優勢:
- 讓快取的效率更高。許多應用程式可以很方便的快取單表查詢對應的結果物件。另外對於MYSQL的查詢快取來說,如果關聯中的某個表發生了變化,那麼就無法使用查詢快取了,而拆分後,如果某個表很少改變,那麼基於該表的查詢就可以重複利用查詢快取結果了。
- 將查詢分解後,執行單個查詢就可以減少鎖的競爭。
- 在應用層做關聯,可以更容易對資料庫進行拆分,更容易做到高效能和可擴充套件。
- 查詢本身效率也可能會有提升。
- 可以減少冗餘記錄的查詢。在應用層做關聯查詢,意味著對於某條記錄應用只需查詢一次,而在資料庫中做關聯查詢,則可能需要重複地訪問一部分資料(冗餘資料引起)。從這點看,這樣的重構還可能會減少網路和記憶體的消耗。
- 更進一步,這樣做相當於在應用中實現了雜湊關聯,而不是使用MySQL的巢狀迴圈關聯。某些場景雜湊關聯的效率要高的多。
結論:恰當的時候選擇恰當的方法,遵循以下原則:
- 資料量小時,聯合查詢比較簡便,10萬記錄以上不建議
- 資料量大時,單表多次查詢好處多多。