前言
平時在寫一些小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萬記錄以上不建議
- 數據量大時,單表多次查詢好處多多。