一手好SQL是如何練成的
一. Mysql性能
1.1 最大數據量
拋開數據量和併發數,談性能都是耍流氓。MySQL沒有限制單表最大記錄數,它取決於操作系統對文件大小的限制。
文件系統 | 單文件大小限制 |
---|---|
FAT32 | 最大4GB |
NTFS | 最大64GB |
NTFS5.0 | 最大2TB |
EXT2 | 塊大小爲1024個字節,文件最大容量16GB;塊大小爲4096個字節,文件最大容量2TB |
EXT3 | 塊大小爲4KB,文件最大容量4TB |
EXT4 | 理論上可以大於16TB |
《阿里巴巴Java開發手冊》提出單表超過500萬行或者單表的容量超過2GB,才推薦分庫分表。性能由綜合因素決定,拋開業務的複雜度,影響程度依次是硬件配置、Mysql配置、數據庫設計、索引優化。500萬這個值僅供參考。博主層操縱過超過4億行數據的單表,分頁查詢最新的20條記錄耗時0.6秒,sql大致如下:
select field_1,field_2 from table where id < #{prePageMinId} order by id desc limit 20;
prePageMinId是上一頁數據記錄的最小ID。雖然當時查詢速度還湊合,隨着數據不斷增長,有朝一日必定不堪重負。分庫分表是個週期長而風險高的大活兒,應該儘可能在當前結構上優化,比如升級硬件、遷移歷史數據等等,實在沒轍了再分。對分庫分表感興趣的同學可以閱讀分庫分表的基本思想。
1.2 最大併發數
併發數是指同一個時間段數據庫能處理的請求個數,由max_connections和max_user_connections決定。max_connections指的是Mysql實例的最大連接數,上限值是16384。max_user_connections是指每個數據庫用戶的最大連接數。Mysql會爲每個連接提供緩衝區,但這意味着要消耗更多的內存。如果連接數設置太高,會導致硬件吃不消,太低又不能充分利用硬件。一般要求兩者的比值超過10%,計算方法如下:
max_user_connections / max_connections * 100%
查看最大連接數和數據庫用戶最大連接數:
show variable like ‘%max_connections%’;
show variable like ‘%max_user_connections%’;
在配置文件my.cnf中修改最大連接數:
[mysqld]
max_connections = 100
max_used_connections = 20
1.3 查詢耗時0.5秒
建議將單次查詢耗時控制在0.5秒之內,0.5秒是一個經驗值,源於用戶體驗的3秒原則。如果用戶的操作在3秒內沒有收到響應,將會厭煩甚至退出。
響應時間 = 客戶端UI渲染耗時 + 網絡請求耗時 + 應用程序處理耗時 + 查詢數據庫耗時
0.5秒就是留給數據庫的1/6處理時間。
1.4 實施原則
相比NoSQL數據庫,Mysql數據庫比較嬌氣,它擴容難、容量小併發低、SQL約束太多。如今大家都會搞點分佈式,應用程序擴容比數據庫擴容容易的多,所以實施的原則是數據庫少幹活,應用程序多幹活。
- 充分利用索引但不濫用索引,須知索引也會消耗磁盤和CPU。
- 不推薦使用數據庫函數格式化數據,儘可能交給應用程序處理。
- 不推薦使用外鍵約束,用應用程序保證數據的準確性。
- 寫多讀少的場景,不推薦使用唯一索引,用應用程序保證唯一性。
- 適當的冗餘字段,嘗試創建中間表,用應用程序計算中間結果,用空間換時間。
- 不允許執行極度耗時的事務,配合應用程序拆分成更小的事務。
- 預估重要數據表(訂單表)的負載和數據增長態勢,提前優化。
二. 數據表設計
2.1 數據類型
數據類型的選擇原則: 更簡單或佔用空間更小。
- 如果長度能夠滿足,整形儘量使用tinyint、smallint、mediumint而非int。
- 如果字符串長度確定,採用char類型。
- 如果varchar能夠滿足,不採用text類型。
- 精度要求較高的使用decimal類型,也可以使用BIGINT,比如精確兩位小數就乘以100後保存。
- 儘量採用timestamp而非datetime。
char、varchar、text的特性:
- char的長度固定,即每條數據佔用等長字節空間。
- varchar可變長度,可以設置最大長度,適合用在長度可變的屬性上。
- text不設置長度,當不知道屬性的最大長度時,使用text。
從空間上來看,當varchar大於某些數值的時候,會自動轉換成text,規則如下:
- 大於varchar(255)變成tinytext
- 大於varchar(500)變成text
- 大於varchar(20000)變成mediumtext
總體來說:
- char存定長,執行速度快,但存在空間浪費的可能,會處理尾部空格,存儲的字符上限255。
- varchar變長,不存在空間浪費,不處理尾部空格,雖然上限存儲65535個字符,實際上只有65532的字符空間可用。
- text 存變長大數據,不存在空間浪費,不處理尾部空格,上限存儲65525個字符,並且會使用額外的空間存儲數據長度,因此可以用滿65525個字符。
類型 | 佔據節點 | 描述 |
---|---|---|
datetime | 8字節 | ‘1000-01-01 00:00:00.000000’ to '9999-12-31 23:59:59.999999’ |
timestamp | 4字節 | ‘1970-01-01 00:00:01.000000’ to ‘2038-01-19 03:14:07.999999’ |
相比datetime,timestamp佔用更少的空間,以UTC的格式存儲自動轉換時區。
Q: 爲什麼char類型數據執行效率比varchar和text都要高?
2.1 避免空值
Mysql中null依然會佔用空間,也會使用索引,導致索引統計時更加複雜。從Null值更新到非Null值無法做到原地更新,容易發生索引分裂影響性能。儘可能的將Null值用更有意義的值來代替,也能避免SQL語句裏包含is not null的判斷。
2.2 text類型優化
由於text字段存儲大量數據,表容量會很快漲上去,影響同表內其它字段的查詢性能。建議抽取出來放到子表中,用業務主鍵來關聯。
三. 索引優化
3.1 索引分類
- 普通索引:
ALTER TABLE
table_name
ADD INDEX index_name (column
)
普通索引(由關鍵字KEY或INDEX定義的索引)的唯一任務是加快對數據的訪問速度。因此,應該只爲那些最經常出現在查詢條件(WHEREcolumn=)或排序條件(ORDERBYcolumn)中的數據列創建索引。只要有可能,就應該選擇一個數據最整齊、最緊湊的數據列(如一個整數類型的數據列)來創建索引。
- 唯一索引:
ALTER TABLE
table_name
ADD UNIQUE (column
)
普通索引允許被索引的數據列包含重複的值。比如說,因爲人有可能同名,所以同一個姓名在同一個“員工個人資料”數據表裏可能出現兩次或更多次。
如果能確定某個數據列將只包含彼此各不相同的值,在爲這個數據列創建索引的時候就應該用關鍵字UNIQUE把它定義爲一個唯一索引。這麼做的好處:一是簡化了MySQL對這個索引的管理工作,這個索引也因此而變得更有效率;二是MySQL會在有新記錄插入數據表時,自動檢查新記錄的這個字段的值是否已經在某個記錄的這個字段裏出現過了;如果是,MySQL將拒絕插入那條新記錄。也就是說,唯一索引可以保證數據記錄的唯一性。
- 主鍵索引
ALTER TABLE
table_name
ADD PRIMARY KEY (column
)
主鍵索引是一個特殊的唯一索引,用於唯一標識數據表的某條記錄,不允許出現空值。
- 外鍵索引
[CONSTRAINT [symbol]] FOREIGN KEY [index_name] (index_col_name, …) REFERENCES tbl_name(index_col_name,…) [ON DELETE reference_option] [ON UPDATE reference_option]
點我參考
- 複合索引(組合索引)
ALTER TABLE
table_name
ADD INDEX index_name (column1
,column2
,column3
)
在多個字段上建立的索引,能夠加速符合條件的查詢速度。
- 全文索引
創建方法:
ALTER TABLEtable_name
ADD FULLTEXT INDEX index_name(’column
)
使用方法
:
SELECT * FROMstudent
WHERE MATCH(name
,address
) AGAINST('小明, ‘美國’)
用於海量文本的查詢,Mysql5.6之後的InnoDB和MyISAM均支持全文檢索,但由於查詢精度和擴展性不佳,因此更多的企業選擇使用Elastisearch來做全文檢索。
3.2 索引的優化
- 分頁查詢很重要,如果查詢的數據量超過30%,則Mysql不會使用索引。
- 單表的索引數不要超過5個,單個索引字段不超過5個。
- 字符串可以使用前綴索引,前綴的長度控制在5-8個字符。
- 如果字段的唯一性太低,那麼增加索引將沒有意義,如: 是否刪除、性別等。
- 合理的使用覆蓋索引,如下所示:
select login_name, nick_name from member where login_name = ?
login_name, nick_name兩個字段建立組合索引,比login_name簡單索引要更快。
四. SQL優化
4.1 分批處理
一次性對大量數據進行處理(更新、刪除、不帶分頁的查詢)可能會導致整個Mysql數據庫處於阻塞狀態
比如:
update status=0 FROM
coupon
WHERE expire_date <= #{currentDate} and status=1;
如果大量優惠券需要更新爲不可用狀態,執行這條SQL可能會堵死其他SQL,所以我們需要使用分批處理的思想。
僞代碼如下:
int pageNo = 1;
int PAGE_SIZE = 100;
while(true) {
List<Integer> batchIdList = queryList('select id FROM `coupon` WHERE expire_date <= #{currentDate} and status = 1 limit #{(pageNo-1) * PAGE_SIZE},#{PAGE_SIZE}');
if (CollectionUtils.isEmpty(batchIdList)) {
return;
}
update('update status = 0 FROM `coupon` where status = 1 and id in #{batchIdList}')
pageNo ++;
}
4.2 操作符<>和!=的優化
通常<>和!=操作符無法使用索引,舉個例子,查詢金額不爲100的訂單:
select id from order where amount != 100;
如果金額爲100的訂單非常少(數據分佈不均勻),那麼本次查詢很可能使用的是"全表掃描(full table scan)"。介於這種不確定性,我們採用union聚合搜索,方法如下:
(select id from orders where amount > 100)
union all
(select id from orders where amount < 100 and amount > 0)
4.3 OR優化
在Innodb引擎下,or無法使用組合索引。比如:
select id,product_name from orders where mobile_no = ‘13421800407’ or user_id = 100;
OR無法命中mobile_no + user_id的組合索引,可採用union,如下所示:
(select id,product_name from orders where mobile_no = ‘13421800407’)
union
(select id,product_name from orders where user_id = 100);
此時id和product_name字段都有索引,查詢才最高效。
4.4 IN優化
- IN適合主表大,子表小。EXISTS適合主表小子表大。由於查詢優化器的不斷升級,很多場景這兩者性能差不多一樣了。
- 嘗試改成join查詢。舉例如下:
select id from orders where user_id in (select id from user where level = ‘VIP’);
採用JOIN如下所示:
select o.id from orders o left join user u on o.user_id = u.id where u.level = ‘VIP’;
4.5 不做列運算
通常在查詢條件中做列運算會導致索引失效,比如:
查詢當日訂單
select id from order where date_format(create_time,’%Y-%m-%d’) = ‘2019-07-01’;
date_format函數會導致這個查詢無法使用索引,改寫後:
select id from order where create_time between ‘2019-07-01 00:00:00’ and ‘2019-07-01 23:59:59’;
4.6 避免Select all
如果不查詢表中的所有列,避免使用select *,因爲它會進行全表掃描,不能有效的利用索引。
4.7 Like優化
like用於模糊查詢,舉個例子(field已建立索引):
SELECT column FROM table WHERE field like ‘%keyword%’;
這個查詢未命中索引,換成下面的寫法:
SELECT column FROM table WHERE field like ‘keyword%’;
去除了前面的%查詢將會命中索引,但是產品經理一定要前後模糊匹配呢?全文索引fulltext可以嘗試一下,但Elasticsearch纔是終極武器。
4.8 Join優化
join的實現是採用Nested Loop Join算法,通過驅動表的結果集作爲基礎數據,根據兩張表的連接條件,將驅動表的每一條記錄放在另一張表中查詢數據,然後將查詢到的記錄合併,得到最終結果。如果有多個join,則將前面的結果作爲循環數據,再到後面一張表中查詢數據。
- 驅動表和被驅動表儘可能的增加查詢條件和滿足ON的條件,而少用where,用小結果集驅動大結果集。
- 被驅動表的join字段上加上索引,但執行時發現無法使用索引時,請設置足夠大的Join Buffer Size。
- 禁止Join連接三個以上表,嘗試增加冗餘字段。
4.9 Limit優化
limit作爲分頁查詢時,越往後翻頁,性能越差,解決的原則時: 縮小掃描範圍
select * from orders order by id desc limit 100000,10
耗時0.4秒
select * from orders order by id desc limit 1000000,10
耗時5.2秒
先篩選出ID縮小查詢範圍,再進行分頁查詢,寫法如下:
select * from orders where id > (select id from orders order by id desc limit 1000000, 1) order by id desc limit 0,10
耗時0.5秒
如果查詢條件僅有主鍵ID,寫法如下:
select id from orders where id between 1000000 and 1000010 order by id desc
耗時0.3秒
如果依然很慢,那就只好用遊標了,具體可以查看: JDBC使用遊標實現分頁查詢的方法
五. 其它數據庫
作爲一名後端開發人員,務必精通作爲存儲核心的MySQL或SQL Server,也要積極關注NoSQL數據庫,他們已經足夠成熟並被廣泛採用,能解決特定場景下的性能瓶頸。
分類 | 數據庫 | 特性 |
---|---|---|
鍵值型 | Memcache | 用於內容緩存,大量數據的高訪問負載 |
鍵值型 | Redis | 用於內容緩存,比Memcache支持更多的數據類型,並能持久化數據 |
列式存儲 | HBase | Hadoop體系的核心數據庫,海量結構化數據存儲,大數據必備。 |
文檔型 | MongoDb | 知名文檔型數據庫,也可以用於緩存 |
文檔型 | CouchDB | Apache的開源項目,專注於易用性,支持REST API |
文檔型 | SequoiaDB | 國內知名文檔型數據庫 |
圖型 | Neo4J | 用於社交網絡構建關係圖譜,推薦系統等 |