目錄
About MySQL
MySQL(讀作/maɪ ˈsiːkwəl/“My Sequel”)是一個開放原始碼的關聯式資料庫管理系統,原開發者為瑞典的MySQL AB公司,目前為Oracle旗下產品。
被甲骨文公司收購後,自由軟體社群們對於Oracle是否還會持續支援MySQL社群版(MySQL之中唯一的免費版本)有所隱憂,因此MySQL的創始人麥克爾·維德紐斯以MySQL為基礎,成立分支計劃MariaDB。原先一些使用MySQL的開源軟體,部分轉向了MariaDB或其它的資料庫。
不可否認的是,MySQL由於其效能高、成本低、可靠性好,已經成為最流行的開源資料庫之一,隨著MySQL的不斷成熟,它也逐漸用於更多大規模網站和應用,非常流行的開源軟體組合LAMP中的“M”指的就是MySQL。
Why MySQL
在眾多開源免費的關係型資料庫系統中,MySQL有以下比較出眾的優勢:
- 執行速度快
- 易使用
- SQL語言支援
- 移植性好
- 功能豐富
- 成本低廉
對於其中執行速度,根據官方介紹,MySQL 8.0 比之前廣泛使用的版本 MySQL 5.7 有了兩倍的提升。
在其官方的Benchmarks中,只讀的效能超過了每秒一百萬次:
讀寫的效能接近每秒二十五萬次:
MySQL Index
Why Index
從概念上講,資料庫是資料表的集合,資料表是資料行和資料列的集合。當你執行一個SELECT語句
從資料表中查詢部分資料行的時候,得到的就是另外一個資料表和資料行的集合。
當然,我們都希望獲得這個新的集合的時間儘可能地短,效率儘可能地高,這就是優化查詢。
提升查詢速度的技術有很多,其中最重要的就是索引。當你發現自己的查詢速度慢的時候,最快解決問題的方法就是使用索引。索引的使用是影響查詢速度的重要因素。在使用索引之前其他的優化查詢的動作純粹是浪費時間,只有合理地使用索引之後,才有必要考慮其他優化方式。
索引是如何工作的
首先,在你的MySQL上建立t_user_action_log
表,方便下面進行演示。
CREATE DATABASE `ijiangtao_local_db_mysql` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE ijiangtao_local_db_mysql;
DROP TABLE IF EXISTS t_user_action_log;
CREATE TABLE `t_user_action_log` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
`name` VARCHAR(32) DEFAULT NULL COMMENT '使用者名稱',
`ip_address` VARCHAR(50) DEFAULT NULL COMMENT 'IP地址',
`action` INT4 DEFAULT NULL COMMENT '操作:1-登入,2-登出,3-購物,4-退貨,5-瀏覽',
`create_time` TIMESTAMP COMMENT '建立時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.2', 1, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.1', 2, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.2', 1, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.3', 1, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.2', 2, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.4', 1, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.2', 2, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.1', 1, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.2', 2, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.2', 1, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.2', 3, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.2', 5, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.2', 2, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.2', 2, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.2', 3, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.2', 3, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.2', 5, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.2', 3, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.2', 3, CURRENT_TIMESTAMP);
INSERT INTO t_user_action_log (name, ip_address, `action`, create_time) values ('LiSi', '8.8.8.2', 4, CURRENT_TIMESTAMP);
複製程式碼
假如我們要篩選 action
為2
的所有記錄,SQL如下:
SELECT id, name, ip_address FROM t_user_action_log WHERE `action`=2;
複製程式碼
通過查詢分析器explain
分析這條查詢語句:
EXPLAIN SELECT id, name, ip_address FROM t_user_action_log WHERE `action`=2;
複製程式碼
分析結果如下:
id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | t_user_action_log | ALL | 1 | 100.00 | Using where |
其中type
為ALL
表示要進行全表掃描。這樣效率無疑是極慢的。
下面為action
列新增索引:
ALTER TABLE t_user_action_log ADD INDEX (`action`);
複製程式碼
然後再次執行查詢分析,結果如下:
id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | t_user_action_log | ref | action | action | 5 | const | 1 | 100.00 |
我們看到這次查詢就使用索引了。加索引前Extra
的值是Using Where,加索引後Extra
的值為空。
那麼為什麼索引會提高查詢速度呢?原因是索引會根據索引值進行分類,這樣就不用再進行全表掃描了。
比如上圖,action
值為2
的索引值分類儲存在了索引空間,可以快速地查詢到索引值所對應的列。
如何使用
下面介紹一下如何使用SQL建立、檢視和刪除索引。
建立索引
三種方式:
- 使用
CREATE INDEX
建立,語法如下:
CREATE INDEX indexName ON tableName (columnName(length));
複製程式碼
例如我們對ip_address
這一列建立一個長度為16的索引:
CREATE INDEX index_ip_addr ON t_user_action_log (ip_address(16));
複製程式碼
- 使用
ALTER
語句建立,語法如下:
ALTER TABLE tableName ADD INDEX indexName(columnName);
複製程式碼
ALTER
語句建立索引前面已經有例子了。下面提供一個設定索引長度的例子:
ALTER TABLE t_user_action_log ADD INDEX ip_address_idx (ip_address(16));
SHOW INDEX FROM t_user_action_log;
複製程式碼
Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
---|---|---|---|---|---|---|---|---|---|---|---|---|
t_user_action_log | 1 | ip_address_idx | 1 | ip_address | A | 1 | 16 | YES | BTREE |
- 建表的時候建立索引:
CREATE TABLE tableName(
id INT NOT NULL,
columnName columnType,
INDEX [indexName] (columnName(length))
);
複製程式碼
檢視索引
可以通過show
語句檢視索引:
SHOW INDEX FROM t_user_action_log;
複製程式碼
Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
---|---|---|---|---|---|---|---|---|---|---|---|---|
t_user_action_log | 0 | PRIMARY | 1 | id | A | 199,368 | BTREE | |||||
t_user_action_log | 1 | action | 1 | action | A | 4 | YES | BTREE | ||||
t_user_action_log | 1 | index_ip_addr | 1 | ip_address | A | 1 | 16 | YES | BTREE |
刪除索引
使用ALTER
命令可以刪除索引,例如:
ALTER TABLE t_user_action_log DROP INDEX index_ip_addr;
複製程式碼
索引的使用原則
索引由於其提供的優越的查詢效能,似乎不使用索引就是一個愚蠢的行為了。但是使用索引,是要付出時間和空間的代價的。因此,索引雖好不可貪多。
下面介紹幾個索引的使用技巧和原則,在使用索引之前,你應該對它們有充分的認識。
寫操作比較頻繁的列慎重加索引
索引在提高查詢速度的同時,也由於需要更新索引而帶來了降低插入、刪除和更新帶索引列的速度的問題。一張資料表的索引越多,在寫操作的時候效能下降的越厲害。
索引越多佔用磁碟空間越大
與沒有加索引比較,加索引會更快地使你的磁碟接近使用空間極限。
不要為輸出列加索引
為查詢條件、分組、連線條件的列加索引,而不是為查詢輸出結果的列加索引。
例如下面的查詢語句:
select ip_address from t_user_action_log
where name='LiSi'
group by action
order by create_time;
複製程式碼
所以可以考慮增加在 name
action
create_time
列上,而不是 ip_address
。
考慮維度優勢
例如action
列的值包含:1、2、3、4、5,那麼該列的維度就是5。
維度越高(理論上維度的最大值就是資料行的總數),資料列包含的獨一無二的值就越多,索引的使用效果越好。
對於維度很低的資料列,索引幾乎不會起作用,因此沒有必要加索引。
例如性別列的值只有男和女,每種查詢結果佔比大約50%。一般當查詢優化處理器發現查詢結果超過全表的30%的時候,就會跳過索引,直接進行全表掃描。
對短小的值加索引
對短小的值加索引,意味著索引所佔的空間更小,可以減少I/O活動,同時比較索引的速度也更快。
尤其是主鍵,要儘可能短小。
另外,InnoDB使用的是聚集索引(clustered index),也就是把主鍵和資料行儲存在一起。主鍵之外的其他索引都是二級索引,這些二級索引也保留著一份主鍵,這樣在查詢到索引以後,就可以根據主鍵找到對應的資料行。如果主鍵太長的話,會造成二級索引佔用的空間變大。
比如下面的action索引儲存了對應行的id。
為字串字首加索引
前邊已經講過短小索引的種種好處了,有時候一個字串的前幾個字元就能唯一標識這條記錄,這個時候設定索引的長度就是非常划算的做法。
前面已經提供了設定索引length
的例子,這裡就不舉例子了。
複合索引的左側索引
建立複合索引的語法如下:
CREATE INDEX indexName ON tableName (column1 DESC, column2 DESC, column3 ASC);
複製程式碼
我們可以看到,最左側的column1索引總是有效的。
索引加鎖
對於InnoDB來說,索引可以讓查詢鎖住更少的行,從而可以在併發情況下擁有更佳表現。
下面演示一下查詢鎖與索引之間的關係。
前面使用的t_user_action_log
表目前有一個id
為主鍵,還有一個二級索引action
。
下面這條語句的修改範圍是id
值為1
2
3
4
所在的行,查詢鎖會鎖住id
值為1
2
3
4
5
所在的行。
update ijiangtao_local_db_mysql.t_user_action_log set name='c1' where id<5;
複製程式碼
- 首先建立資料庫連線1,開啟事務,並執行update語句
set autocommit=0;
begin;
update ijiangtao_local_db_mysql.t_user_action_log set name='c1' where id<5;
複製程式碼
- 然後開啟另外一個連線2,分別執行下面幾個update語句
-- 沒有被鎖
update ijiangtao_local_db_mysql.t_user_action_log set name='c2' where id=6;
-- 被鎖
update ijiangtao_local_db_mysql.t_user_action_log set name='c2' where id=5;
複製程式碼
你會發現id=5
的資料行已經被鎖定,id=6
的資料行可以正常提交。
- 連線1提交事務,連線2的
id=1
和id=5
的資料行可以update成功了。
-- 在連線1提交事務
commit;
複製程式碼
- 如果不使用索引
ip_address
沒有索引的話,會鎖定全表。
連線1開啟事務以後commit;
之前,連線2對該表的update全部需要等待連線1釋放鎖。
set autocommit=0;
begin;
update ijiangtao_local_db_mysql.t_user_action_log set name='c1' where ip_address='8.8.8.1';
複製程式碼
覆蓋索引
如果索引包含滿足查詢的所有資料,就被稱為覆蓋索引(Covering Indexes),覆蓋索引非常強大,可以大大提高查詢效能。
覆蓋索引高效能的原因是:
- 索引通常比記錄要小,覆蓋索引查詢只需要讀索引,而不需要讀記錄。
- 索引都按照值的大小進行順序儲存,相比與隨機訪問記錄,需要更少的I/0。
- 大多數資料引擎能更好的快取索引,例如MyISAM只快取索引。
ijiangtao_local_db_mysql
表的action
列包含索引。使用explain
分析下面的查詢語句,對於索引覆蓋查詢(index-covered query),分析結果Extra
的值是Using index
,表示使用了覆蓋索引 :
explain select `action` from ijiangtao_local_db_mysql.t_user_action_log;
複製程式碼
id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | t_user_action_log | index | action | 5 | 199,703 | 100.00 | Using index |
聚簇索引
聚簇索引(Clustered Indexes)保證關鍵字的值相近的元組儲存的物理位置也相同,且一個表只能有一個聚簇索引。
字串型別不建議使用聚簇索引,特別是隨機字串,因為它們會使系統進行大量的移動操作。
並不是所有的儲存引擎都支援聚簇索引,目前InnoDB支援。
如果使用聚簇索引,最好使用AUTO_INCREMENT
列作為主鍵,應該儘量避免使用隨機的聚簇主鍵。
從物理位置上看,聚簇索引表比非聚簇的索引表,有更好的訪問效能。
選擇合適的索引型別
從資料結構角度來看,MySQL支援的索引型別有B樹索引、Hash索引等。
- B樹索引
B樹索引對於<、<=、 =、 >=、 >、 <>、!=、 between查詢,進行精確比較操作和範圍比較操作都有比較高的效率。
B樹索引也是InnoDB儲存引擎預設的索引結構。
- Hash索引
Hash索引僅能滿足=、<=>、in查詢。
Hash索引檢索效率非常高,索引的檢索可以一次定位,不像B樹索引需要從根節點到枝節點,最後才能訪問到頁節點這樣多次的I/O訪問,所以Hash索引的查詢效率要遠高於B樹索引。但Hash索引不能使用範圍查詢。
查詢優化建議
下面提供幾個查詢優化的建議。
使用explain分析查詢語句
前面已經演示過如何使用explain
命令分析查詢語句了,這裡再解釋一下其中幾個有參考價值的欄位的含義:
select_type
select_type表示查詢中每個select子句的型別,一般有下面幾個值:
-
SIMPLE 簡單SELECT,不使用UNION或子查詢等。
-
PRIMARY 查詢中若包含任何複雜的子部分,最外層的select被標記為PRIMARY。
-
UNION UNION中的第二個或後面的SELECT語句。
-
DEPENDENT UNION UNION中的第二個或後面的SELECT語句,取決於外面的查詢。
-
UNION RESULT UNION的結果。
-
SUBQUERY 子查詢中的第一個SELECT。
-
DEPENDENT SUBQUERY 子查詢中的第一個SELECT,取決於外面的查詢。
-
DERIVED 派生表的SELECT, FROM子句的子查詢。
-
UNCACHEABLE SUBQUERY 一個子查詢的結果不能被快取,必須重新評估外連結的第一行。
type
type表示MySQL在表中找到所需行的方式,又稱“訪問型別”,常用的型別有:
ALL, index, range, ref, eq_ref, const, system, NULL。
從左到右,效能從差到好。
-
ALL: Full Table Scan,MySQL將遍歷全表以找到匹配的行。
-
index: Full Index Scan,index與ALL區別為index型別只遍歷索引樹。
-
range: 只檢索給定範圍的行,使用一個索引來選擇行。
-
ref: 表示上述表的連線匹配條件,即哪些列或常量被用於查詢索引列上的值。
-
eq_ref: 類似ref,區別就在使用的索引是唯一索引,對於每個索引鍵值,表中只有一條記錄匹配,簡單來說,就是多表連線中使用primary key或者 unique key作為關聯條件。
-
const: 當MySQL對查詢某部分進行優化,並轉換為一個常量時,使用這些型別訪問。 如將主鍵置於where列表中,MySQL就能將該查詢轉換為一個常量。
-
NULL: MySQL在優化過程中分解語句,執行時甚至不用訪問表或索引,例如從一個索引列裡選取最小值可以通過單獨索引查詢完成。
Key
key列顯示MySQL實際決定使用的鍵(索引),如果沒有選擇索引,鍵是NULL。
possible_keys
possible_keys指出MySQL能使用哪個索引在表中找到記錄,查詢涉及到的欄位上如果存在索引則該索引將被列出,但不一定被查詢使用。
ref
ref表示上述表的連線匹配條件,即哪些列或常量被用於查詢索引列上的值。
rows
rows表示MySQL根據表統計資訊,以及索引選用的情況,找到所需記錄需要讀取的行數。這個行數是估算的值,實際行數可能不同。
宣告NOT NULL
當資料列被宣告為NOT NULL以後,在查詢的時候就不需要判斷是否為NULL,由於減少了判斷,可以降低複雜性,提高查詢速度。
如果要表示資料列為空,可以使用0等代替。
考慮使用數值型別代替字串
MySQL對數值型別的處理速度要遠遠快於字串,而且數值型別往往更加節省空間。
例如對於“Male”和“Female”可以用“0”和“1”進行代替。
考慮使用ENUM型別
如果你的資料列的取值是確定有限的,可以使用ENUM型別代替字串。因為MySQL會把這些值表示為一系列對應的數字,這樣處理的速度會提高很多。
CREATE TABLE shirts (
name VARCHAR(40),
size ENUM('x-small', 'small', 'medium', 'large', 'x-large')
);
INSERT INTO shirts (name, size) VALUES ('dress shirt','large'), ('t-shirt','medium'),
('polo shirt','small');
SELECT name, size FROM shirts WHERE size = 'medium';
複製程式碼
總結
索引是一個單獨的,儲存在磁碟上的資料結構,索引對資料表中一列或者多列值進行排序,索引包含著對資料表中所有資料的引用指標。
本教程從MySQL開始講起,又介紹了MySQL中索引的使用,最後提供了使用索引的幾條原則和優化查詢的幾個方法。
無論你是DBA還是軟體開發,菜鳥程式設計師還是資深工程師,相信本節提到的關於索引的知識,對你都會有所幫助。