MySQL優化小建議
背景
“那啥,你過來一下!”
“怎麽了?我代碼都單元測試了的,沒出問題啊!”我一臉懵逼跑到運維大佬旁邊。
“你看看!你看看!多少條報警,趕快優化一下!”
運維大佬短信列表裏面50多條MySQL CPU 100%報警短信。再看看項目名稱不就是我前幾天剛發布的項目嗎!?
我心底一沈,趕快賠上笑臉。“這個一定優化,馬上優化!那個,能不能看下數據庫監控日誌...”
運維大佬又數落了我幾句,然後調開了數據庫監控日誌。
那家夥...每秒300多的連接數,幾乎快要封頂的全表掃描數,還有大紅色CPU警報。。。
“那個,能不能看看nginx訪問日誌...我看下訪問量...”我弱弱地說到。
運維大佬不情願的跑了下下面的語句:
grep -c come access.log
come這個接口是其中一個請求量比較大的接口,結果是600多萬。那個時候才中午,周末高峰期估計一天得有上千萬吧,
我撇了撇嘴,心裏想著這麽高的請求量,當初那麽摳門只給我一臺低配數據庫還好意思說,不過嘴上肯定是:“好好好,請求量不是很大,看來是數據庫問題,我立刻去優化一下!”
“給它弄一個讀寫分離不就行了嗎!?”這時另外一個運維大佬湊了過來,隨意地揮了揮手。。。
你問我DBA去哪兒了?DBA當時有點忙,只說讓我自己檢查一下。。。
優化思路
我這個項目由於上線之前比較趕,所以前期並沒有管數據庫設計方面的一些問題,如今隨著遊戲接入,請求量劇增才暴露出來。(其實是前期加班加煩了懶得搞)
這個問題,並不需要增加數據庫硬件配置和增加讀寫分離這種高端手段就能解決,我自個兒挖了多少坑,心裏還是有點碧樹的。
詳細的MySQL優化步驟如下:
- 檢查數據表結構,改善不完善設計
- 跑一遍主要業務,收集常用的數據庫查詢SQL
- 分析查詢SQL,適當拆分,添加索引等優化查詢
- 優化SQL的同時,優化代碼邏輯
- 添加本地緩存和redis緩存
這個項目是原生PHP寫的,以上這些只能自己做了。
檢查數據表結構
因為比較菜,回去看設計的表結構,真是慘不忍睹。
盡可能不要使用NULL值
因為建表的時候,如果不對創建的值設置默認值,MySQL都會設置默認為NULL
。那麽為啥用NULL
不好呢?
NULL
使得索引維護更加復雜,強烈建議對索引列設置NOT NULL
NOT IN
、!=
等負向條件查詢在有NULL
值的情況下返回永遠為空結果,查詢容易出錯NULL
列需要一個額外字節作為判斷是否為NULL
的標誌位- 使用
NULL
時和該列其他的值可能不是同種類型,導致問題。(在不同的語言中表現不一樣) - MySQL難以優化對可為
NULL
的列的查詢
所以對於那些以前偷懶的字段,手動設置一個默認值吧,空字符串呀,0呀補上。
雖然這種方法對於MySQL的性能來說沒有提升多少,但是這是一個好習慣,而且以小見大,不要忽略這些細節。
添加索引
對於經常查詢的字段,請加上索引,有索引和沒有索引的查詢速度相差十倍甚至更多。
- 一般來說,每張表都需要有一個主鍵
id
字段 - 常用於查詢的字段應該設置索引
varchar
類型的字段,在建立索引的時候,最好指定長度- 查詢有多個條件時,優先使用具有索引的條件
- 像
LIKE
條件這樣的模糊搜索對於字段索引是無效的,需要另外建立關鍵詞索引來解決 - 請盡量不要在數據庫層面約束表和表之間的關系,這些表之間的依賴應該在代碼層面去解決
當表和表之間有約束時,雖然增刪查的SQL語句變簡單了,但是帶來的負面效果是插入等操作數據庫都會去檢查約束(雖然可以手動設置忽略約束),這樣相當於把一些業務邏輯寫到了數據庫層,不便於維護。
優化表字段結構
數據庫中那些可以用整形表示的數據就不要使用字符串類型,到底是用varchar
還是char
要看字段的可能值。
這種優化往往在數據庫中有大量數據以後是不可行的,最好在數據庫設計之前就設計好。
- 對於那些可能值很有限的列,使用
tinyint
代替VARCHAR
,- 比如記錄移動設備平臺,只有兩個值:android,ios,那麽就可以使用0表示android,1表示ios,這種列一定要寫好註釋
- 為什麽不用
ENUM
呢?ENUM
擴展困難,比如後來移動平臺又增加了一個ipad
,那豈不是懵逼了,而tinyint
加個2就行,而且ENUM
在代碼裏面處理起來特別奇怪,是當成整形呢還是字符串,各個語言不一樣。 - 這種方式,一定要在數據庫註釋或者代碼裏面寫明各個值的含義
- 對於那些定長字符串,可以使用
char
,比如郵編,總是5位 - 對於那些長度未知的字符串,使用
varchar
- 不要濫用
bigint
,比如記錄文章數目的表id
字段,用int
就行了,21億篇文章上限夠了 - 適當打破數據庫範式添加冗余字段,避免查詢時的表連接
查詢的時候,肯定int
類型比varchar
快,因為整數的比較直接調用底層運算器就可以實現,而字符串比較要逐個字符比較。
定長數據比變長數據查詢快,因為比較定長數據與數據之間的偏移是固定的,很容易計算下一個數據的偏移。而變長數據則還需要多一步去查詢下一個數據的偏移量。不過。定長數據可能會浪費更多的存儲空間。
大表拆分
對於那些數據量可能近期會超過500W或者增長很快的表,一定要提前做好垂直分表或者水平分表,當數據量超過百萬以後,查詢速度會明顯下降。
分庫分表盡量在數據庫設計初期敲定方案,否則後期會極大增加代碼復雜性而且不易更改。
垂直分表是按照日期等外部變量進行分表,水平分表是按照表中的某些字段關系,使用hash映射等分表。
分庫分表的前提條件是在執行查詢語句之前,已經知道需要查詢的數據可能會落在哪一個分庫和哪一個分表中。
優化查詢語句
這個才是很多系統數據庫瓶頸的始作俑者。
- 請盡量使用簡單的查詢,避免使用表鏈接
- 請盡量避免全表掃描,會造成全表掃描的語句包括但不限於:
- where子句條件恒真或為空
- 使用
LIKE
- 使用不等操作符(<>、!=)
- 查詢含有
is null
的列 - 在非索引列上使用
or
- 多條件查詢時,請把簡單查詢條件或者索引列查詢置於前面
- 請盡量指定需要查詢的列,不要偷懶使用select *
- 如果不指定,一方面會返回多余的數據,占用寬帶等
- 另一方面MySQL執行查詢的時候,沒有字段時會先去查詢表結構有哪些字段
- 大寫的查詢關鍵字比小寫快一點點
- 使用子查詢會創建臨時表,會比鏈接(JOIN)和聯合(UNION)稍慢
- 在索引字段上查詢盡量不要使用數據庫函數,不便於緩存查詢結果
- 當只要一行數據時,請使用LIMIT 1,如果數據過多,請適當設定LIMIT,分頁查詢
- 千萬不要 ORDER BY RAND(),性能極低
上面是我總結的一些小tips,這些規則是死的,但是業務場景是活的,在實際使用的過程中,比如數據統計,可以適當犧牲性能換取便利。
添加緩存
使用redis等緩存,還有本地文件緩存等,可以極大地減少數據庫查詢次數。緩存這個東西,一定要分析自己系統的數據特點,適當選擇。
- 對於一些常用的數據,比如配置信息等,可以放在緩存中
- 可以在本地緩存數據庫的表結構
- 緩存的數據一定要註意及時更新,還有設置有效期
- 增加緩存務必會增加系統復雜性,一定要註意權衡
優化實例
下面舉幾個簡單的優化查詢例子。首先就是跑一下主要業務,把主要的查詢語句打印到一個文件裏面,然後分析這些語句。
補充一下,在查詢語句前使用關鍵字explain
可以查看查詢執行的具體情況。
看下面的這個查詢語句
select *
from link
where player_id=‘15298635‘ AND gameid=‘10389‘ AND appid=‘200‘
AND action=‘open‘ AND creator=‘android_sdk‘ AND transport=‘{"name":"uusama","age":20}‘
上面這條語句毛病挺多的
- select * 沒有指定查詢列,這個表有20個字段,其實我用到的就幾個
- 查詢列沒有索引,造成全表掃描
- 查詢條件過於冗余,可以適當拆分
- 只需要一條查詢結果,但是沒有限定查詢結果大小
顯然查詢條件很多,而且很多列都是不定長的varchar類型,如果要建立索引,是不是要建立聯合索引呢?
顯然沒有必要,索引的字段越多,MySQL維護的時候越復雜,對性能也會有損耗,像這樣的SQL查詢語句,我們在主要字段上建立索引即可。比如在player_id
字段、gameid
字段、appid
字段上建立索引就夠了。
這樣的查詢語句要結合具體的業務場景來進行分析,比如在我當前的系統中,我是期望上面的語句能夠查詢相同的參數下是否有記錄。其實沒必要使用這麽多條件的查詢。
我只需要使用下面的這條更簡單的查詢語句代替即可。
select id,player_id
from link
where player_id=‘15298635‘
查詢到的記錄條數在100條以下,大部分就只用幾十條記錄,我完全可以在代碼裏面在把查詢結果遍歷一遍判斷即可。這樣不知道有多快呢!
再看下面的這個例子:
select *
from browser
where device_id=‘52‘ AND created>=‘1513735322‘ order by id desc
我只是想查一下這個表裏面某個時間以後的數據。問題大了!
created
字段是timestamp
類型,這樣用是不對的,而且沒有限定行數,這條語句會把數據庫所有的device_id=‘52‘的數據搞出來。
還好device_id
字段設置了索引,要不然必然會導致全表掃描。
修改後的查詢如下:
select *
from browser
where device_id=‘52‘ AND created>=‘2018-03-27 00:00:00‘ order by id desc
我的系統總沒有使用復雜的像表連接和聯合這樣的查詢,這類查詢一定要謹慎使用,能夠拆分的話盡量拆分。
記住下面的速度優先級,兩兩之間相差2個以上數量級
CPU運行速度 > 內存訪問速度 > 磁盤io訪問速度 > 網絡請求速度
MySQL優化小建議