mysql索引結構原理、性能分析與優化

第一部分:基礎知識

索引

官方介紹索引是幫助MySQL高效獲取數據的數據結構。筆者理解索引相當於一本書的目錄,通過目錄就知道要的資料在哪裏, 不用一頁一頁查閱找出需要的資料。

唯一索引(unique index)

強調唯一,就是索引值必須唯一。

創建索引:

create unique index 索引名 on 表名(列名);
alter table 表名 add unique index 索引名 (列名);

刪除索引:

drop index 索引名 on 表名;
alter table 表名 drop index 索引名;

主鍵

主鍵就是唯一索引的一種,主鍵要求建表時指定,一般用auto_increment列,關鍵字是primary key

主鍵創建:

creat table test2 (id int not null primary key auto_increment);

全文索引

InnoDB不支持,MyISAM支持性能比較好,一般在 CHAR、VARCHAR 或 TEXT 列上創建。

Create table 表名( 
    id int not null primary key anto_increment,
    title varchar(100),FULLTEXT(title)
)type=MyISAM;

單列索引與多列索引

索引可以是單列索引也可以是多列索引(也叫複合索引)。按照上面形式創建出來的索引是單列索引,現在先看看創建多列索引:

create table test3 (
    id int not null primary key auto_increment,
    uname char(8) not null default '',
    password char(12) not null,
    INDEX(uname,password)
)type=MyISAM;

注意:INDEX(a, b, c)可以當做a或(a, b)的索引來使用,但不能當作b、c或(b,c)的索引來使用。這是一個最左前綴的 優化方法,在後面會有詳細的介紹,你只要知道有這樣兩個概念。

聚簇索引

一種索引,該索引中鍵值的邏輯順序決定了表中相應行的物理順序。 聚簇索引確定表中數據的物理順序。Mysql中MyISAM 表是沒有聚簇索引的,innodb有(主鍵就是聚簇索引),聚簇索引在下面介紹innodb結構的時有詳細介紹。

查看錶的索引

通過命令:Show index from 表名 如:

mysql> show index from test3;  
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+----+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | 
Packed | Null | Index_type | Comment |  
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+----+
| test3 |          0 | PRIMARY  |        1  |    id          |     A     |   0          |     NULL | 
NULL   |     | BTREE      |         |  
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+
Table:表名
Key_name:什麼類型索引(這裏是主鍵)
Column_name:索引列的字段名
Cardinality:索引基數,很關鍵的一個參數,平均數值組=索引基數/表總數據行,平均數值組越接近1就越有可能利用索引
Index_type:如果索引是全文索引,則是fulltext,這裏是b+tree索引,b+tree也是這篇文章研究的重點之一

第二部分:MyISAM和INNODB索引結構

簡單介紹B-tree B+ tree樹

B-tree結構視圖


一棵m階的B-tree樹,則有以下性質

  1. Ki表示關鍵字值,上圖中,k1<k2<…<ki<k0<Kn(可以看出,一個節點的左子節點關鍵字值<該關鍵字值<右子節點關鍵字值)
  2. Pi表示指向子節點的指針,左指針指向左子節點,右指針指向右子節點。即是:p1[指向值]<k1<p2[指向值]<k2……
  3. 所有關鍵字必須唯一值(這也是創建MyISAM 和innodb表必須要主鍵的原因),每個節點包含一個說明該節點多少個關鍵字,如上圖第二行的i和n
  4. 節點:
    • 每個節點最可以有m個子節點。
    • 根節點若非葉子節點,至少2個子節點,最多m個子節點
    • 每個非根,非葉子節點至少[m/2]子節點或叫子樹([]表示向上取整),最多m個子節點
  5. 關鍵字:
    • 根節點的關鍵字個數1~m-1
    • 非根非葉子節點的關鍵字個數[m/2]-1~m-1,如m=3,則該類節點關鍵字個數:2-1~2
  6. 關鍵字數k和指向子節點個數指針p的關係:
    • k+1=p ,注意根據儲存數據的具體需求,左右指針爲空時要有標誌位表示沒有 B+tree結構示意圖如下:

B+樹是B-樹的變體,也是一種多路搜索樹: * 非葉子結點的子樹指針與關鍵字個數相同 * 爲所有葉子結點增加一個鏈指針(紅點標誌的箭頭)

MyISAM索引結構

MyISAM索引用的B+ tree來儲存數據,MyISAM索引的指針指向的是鍵值的地址,地址存儲的是數據,如下圖:


結構講解:上圖3階樹,主鍵是Col2,Col值就是改行數據保存的物理地址,其中紅色部分是說明標註。

  • 1標註部分也許會迷惑,前面不是說關鍵字15右指針的指向鍵值要大於15,怎麼下面還有15關鍵字?因爲B+tree的所有葉子節點 包含所有關鍵字且是按照升序排列(主鍵索引唯一,輔助索引可以不唯一),所以等於關鍵字的數據值在右子樹
  • 2標註是相應關鍵字存儲對應數據的物理地址,注意這也是之後和InnoDB索引不同的地方之一
  • 2標註也是一個所說MyISAM表的索引和數據是分離的,索引保存在”表名.MYI”文件內,而數據保存在“表名.MYD”文件內,2標註 的物理地址就是“表名.MYD”文件內相應數據的物理地址。(InnoDB表的索引文件和數據文件在一起)
  • 輔助索引和主鍵索引沒什麼大的區別,輔助索引的索引值是可以重複的(但InnoDB輔助索引和主鍵索引有很明顯的區別,這裏 先提醒注意一下)

Innode索引結構

(1)首先有一個表,內容和主鍵索引結構如下兩圖:

Col1

Col2

Col3

1

15

phpben

2

20

mhycoe

3

23

phpyu

4

25

bearpa

5

40

phpgoo

6

45

phphao

7

48

phpxue

…… 


結構上:由上圖可以看出InnoDB的索引結構很MyISAM的有很明顯的區別

  • MyISAM表的索引和數據是分開的,用指針指向數據的物理地址,而InnoDB表中索引和數據是儲存在一起。看紅框1可看出一行 數據都保存了。
  • 還有一個上圖多了三行的隱藏數據列(虛線表),這是因爲MyISAM不支持事務,InnoDB處理事務在性能上併發控制上比較好, 看圖中的紅框2中的DB_TRX_ID是事務ID,自動增長;db_roll_ptr是回滾指針,用於事務出錯時數據回滾恢復;db_row_id 是記錄行號,這個值其實在主鍵索引中就是主鍵值,這裏標出重複是爲了容易介紹,還有的是若不是主鍵索引(輔助索引), db_row_id會找表中unique的列作爲值,若沒有unique列則系統自動創建一個。關於InnoDB跟多事務MVCC點 此: http://www.phpben.com/?post=72

(2)加入上表中Col1是主鍵(下圖標錯),而Col2是輔助索引,則相應的輔助索引結構圖:

可以看出InnoDB輔助索引並沒有保存相應的所有列數據,而是保存了主鍵的鍵值(圖中1、2、3….)這樣做利弊也是很明顯:

  • 在已有主鍵索引,避免數據冗餘,同時在修改數據的時候只需修改輔助索引值。
  • 但輔助索引查找數據事要檢索兩次,先找到相應的主鍵索引值然後在去檢索主鍵索引找到對應的數據。這也是網上很多 mysql性能優化時提到的“主鍵儘可能簡短”的原因,主鍵越長輔助索引也就越大,當然主鍵索引也越大。

MyISAM索引與InnoDB索引相比較

  • MyISAM支持全文索引(FULLTEXT)、壓縮索引,InnoDB不支持
  • InnoDB支持事務,MyISAM不支持
  • MyISAM順序儲存數據,索引葉子節點保存對應數據行地址,輔助索引很主鍵索引相差無幾;InnoDB主鍵節點同時保存數據行,其他輔助索引保存的是主鍵索引的值
  • MyISAM鍵值分離,索引載入內存(key_buffer_size),數據緩存依賴操作系統;InnoDB鍵值一起保存,索引與數據一起載入InnoDB緩衝池
  • MyISAM主鍵(唯一)索引按升序來存儲存儲,InnoDB則不一定
  • MyISAM索引的基數值(Cardinality,show index 命令可以看見)是精確的,InnoDB則是估計值。這裏涉及到信息統計的知識,MyISAM統計信息是保存磁盤中,在alter表或Analyze table操作更新此信息,而InnoDB則是在表第一次打開的時候估計值保存在緩存區內
  • MyISAM處理字符串索引時用增量保存的方式,如第一個索引是‘preform’,第二個是‘preformence’,則第二個保存是‘7,ance‘,這個明顯的好處是縮短索引,但是缺陷就是不支持倒序提取索引,必須順序遍歷獲取索引

第三部分:MYSQL優化

mysql優化是一個重大課題之一,這裏會重點詳細的介紹mysql優化,包括表數據類型選擇,sql語句優化,系統配置與維護優化三類。

表數據類型選擇

  1. 能小就用小。表數據類型第一個原則是:使用能正確的表示和存儲數據的最短類型。這樣可以減少對磁盤空間、內存、cpu緩存的使用。
  2. 避免用NULL,這個也是網上優化技術博文傳的最多的一個。理由是額外增加字節,還有使索引,索引統計和值更復雜。很多還忽略一 個count(列)的問題,count(列)是不會統計列值爲null的行數。更多關於NULL可參考:http://www.phpben.com/?post=71
  3. 字符串如何選擇char和varchar?一般phper能想到就是char是固定大小,varchar能動態儲存數據。這裏整理一下這兩者的區別:

屬性

Char

Varchar

值域大小

最長 字符數 是255(不是字節) ,不管什麼編碼,超過此值則自動截取255個字符保存並沒有報錯。

65535 個字節,開始兩位存儲長度,超過 255 個字符,用 2 位儲存長度,否則 1 位,具體字符長度根據編碼來確定,如 utf8

則字符最長是 21845 個

如何處理字符串末尾空格

去掉末尾空格,取值出來比較的時候自動加上進行比較

Version<=4.1 ,字符串末尾空格被刪掉,version>5.0 則保留

儲存空間

固定空間,比喻char(10) 不管字符串是否有 10 個字符都分配 10 個字符的空間

Varchar 內節約空間,但更新可能發生變化,若varchar(10), 開始若儲存 5個字符,當 update 成 7 個時有 MyISAM 可能把行拆開, innodb 可能分頁,這樣開銷就增大

適用場合

適用於存儲很短或固定或長度相似字符,如 MD5加密的密碼 char(33) 、暱稱 char(8) 等

當最大長度遠大於平均長度並且發生更新的時候。

注意當一些英文或數據的時候,最好用每個字符用字節少的類型,如latin1

  1. 整型、整形優先原則

    Tinyint、smallint、mediumint、int、bigint,分別需要8、16、24、32、64。 
    值域範圍:-2 (n-1)~ 2 (n-1)-1 
    很多程序員在設計數據表的時候很習慣的用int,壓根不考慮這個問題 
    筆者建議:能用tinyint的絕不用smallint 
    誤區:int(1) 和int(11)是一樣的,唯一區別是mysql客戶端顯示的時候顯示多少位。
    整形優先原則:能用整形的不用其他類型替換,如ip可以轉換成整形保存,如商品價格‘50.00元’則保存成50

  2. 精確度與空間的轉換。在存儲相同數值範圍的數據時,浮點數類型通常都會比DECIMAL類型使用更少的空間。FLOAT字段使用4 字節存儲 數據。DOUBLE類型需要8 個字節並擁有更高的精確度和更大的數值範圍,DECIMAL類型的數據將會轉換成DOUBLE類型。

sql語句優化

mysql> create table one (
    id smallint(10) not null auto_increment primary key,  
    username char(8) not null,  
    password char(4) not null,  
    `level` tinyint (1) default 0,  
    last_login char(15) not null,  
    index (username,password,last_login)
    ) engine=innodb;  

這是test表,其中id是主鍵,多列索引(username,password,last_login),裏面有10000多條數據.

| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null |
 Index_type | Comment |  
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+
| one   |        0 | PRIMARY  |           1 | id          | A         |20242 |  NULL | NULL  |    |
 BTREE     |         |  
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+
| one   |        1 | username |            1 | username    | A         |10121 |  NULL | NULL  |     | 
BTREE     |         |  
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+
| one   |        1 | username |            2 | password    | A         |10121 |  NULL | NULL  | YES  |
 BTREE     |         |  
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+
| one   |        1 | username |              3 | last_login  | A         |20242 |  NULL | NULL  |     |
 BTREE      |         |  
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+

最左前綴原則

定義:最左前綴原則指的的是在sql where 子句中一些條件或表達式中出現的列的順序要保持和多索引的一致或以多列索引順序出現,只要 出現非順序出現、斷層都無法利用到多列索引。

舉例說明:上面給出一個多列索引(username,password,last_login),當三列在where中出現的順序如(username,password,last_login)、 (username,password)、(username)才能用到索引,如下面幾個順序(password,last_login)、(passwrod)、(last_login)---這三者不 從username開始,(username,last_login)---斷層,少了password,都無法利用到索引。因爲B+tree多列索引保存的順序是按照索引創 建的順序,檢索索引時按照此順序檢索

測試:以下測試不精確,這裏只是說明如何才能正確按照最左前綴原則使用索引。還有的是以下的測試用的時間0.00sec看不出什麼時間區 別,因爲數據量只有20003條,加上沒有在實體機上運行,很多未可預知的影響因素都沒考慮進去。當在大數據量,高併發的時候,最左前 綴原則對與提高性能方面是不可否認的。

Ps:最左前綴原則中where字句有or出現還是會遍歷全表

能正確的利用索引
  • Where子句表達式 順序是(username)

    mysql> explain select * from one where username='abgvwfnt'; 
    +----+-------------+-------+------+---------------+----------+---------+-------+------+-------------+ 
    | id | select_type | table | type | possible_keys | key | key_len | ref |rows | Extra | 
    +----+-------------+-------+------+---------------+----------+---------+-------+------+-------------+ 
    | 1 | SIMPLE | one | ref | username | username | 24 | const |5 | Using where | 
    +----+-------------+-------+------+---------------+----------+---------+-------+------+-------------+ 
    1 row in set (0.00 sec)

  • Where子句表達式 順序是(username,password)

    mysql> explain select * from one where username='abgvwfnt' and password='123456'; 
    +----+-------------+-------+------+---------------+----------+---------+-------------+------+-------------+ 
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | 
    +----+-------------+-------+------+---------------+----------+---------+-------------+------+-------------+ 
    | 1 | SIMPLE | one | ref | username | username | 43 | const,const | 1 | Using where | 
    +----+-------------+-------+------+---------------+----------+---------+-------------+------+-------------+ 
    1 row in set (0.00 sec)

  • Where子句表達式 順序是(username,password, last_login)

    mysql> explain select * from one where username='abgvwfnt' and password='123456'and last_login='1338251170'; 
    +----+-------------+-------+------+---------------+----------+---------+-------------------+------+-------------+ 
    | id | select_type | table | type | possible_keys | key | key_len | ref| rows | Extra | 
    +----+-------------+-------+------+---------------+----------+---------+-------------------+------+-------------+ 
    | 1 | SIMPLE | one | ref | username | username | 83 | const,const,const | 1 | Using where | 
    +----+-------------+-------+------+---------------+----------+---------+-------------------+------+-------------+ 
    1 row in set (0.00 sec) 
    上面可以看出type=ref 是多列索引,key_len分別是24、43、83,這說明用到的索引分別是(username), (username,password), (username,password, last_login );row分別是5、1、1檢索的數據行都很少,因爲這三個查詢都按照索引前綴原則,可以利用到 索引。

不能正確的利用索引
  • Where子句表達式 順序是(password, last_login)

    mysql> explain select * from one where password='123456'and last_login='1338251170'; 
    +----+-------------+-------+------+---------------+------+---------+------+-------+-------------+ 
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows| Extra | 
    +----+-------------+-------+------+---------------+------+---------+------+-------+-------------+ 
    | 1 | SIMPLE | one | ALL | NULL | NULL | NULL | NULL | 20146 | Using where | 
    +----+-------------+-------+------+---------------+------+---------+------+-------+-------------+ 
    1 row in set (0.00 sec)

  • Where 子句表達式順序是(last_login)

    mysql> explain select * from one where last_login='1338252525'; 
    +----+-------------+-------+------+---------------+------+---------+------+-------+-------------+ 
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows| Extra | 
    +----+-------------+-------+------+---------------+------+---------+------+-------+-------------+ 
    | 1 | SIMPLE | one | ALL | NULL | NULL | NULL | NULL | 20146 | Using where | 
    +----+-------------+-------+------+---------------+------+---------+------+-------+-------------+ 
    1 row in set (0.00 sec) 
    以上的兩條語句都不是以username開始,這樣是用不了索引,通過type=all(全表掃描),key_len=null,rows都很大20146 
    Ps:one表裏只有20003條數據,爲什麼出現20146,這是優化器對錶的一個估算值,不精確的。

  • Where 子句表達式雖然順序是(username,password, last_login)或(username,password)但第一個是有範圍’<’、’>’,’<=’, ’>=’等出現

    mysql> explain select * from one where username>'abgvwfnt' and password ='123456'and last_login='1338251170'; 
    +----+-------------+-------+------+---------------+------+---------+------+-------+-------------+ 
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows| Extra | 
    +----+-------------+-------+------+---------------+------+---------+------+-------+-------------+ 
    | 1 | SIMPLE | one | ALL | username | NULL | NULL | NULL | 20146 | Using where | 
    +----+-------------+-------+------+---------------+------+---------+------+-------+-------------+ 
    1 row in set (0.00 sec) 
    這個查詢很明顯是遍歷所有表,一個索引都沒用到,非第一列出現範圍(password列或last_login列),則能利用索引到首先出 現範圍的一列,也就是“where username='abgvwfnt' and password >'123456'and last_login='1338251170';”或 “where username='abgvwfnt' and password >'123456'and last_login<'1338251170';”索引長度ref_len=43,索引檢索到 password列,所以考慮多列索引的時候把那些查詢語句用的比較的列放在最後(或非第一位)。

  • 斷層,即是where順序(username, last_login)

    mysql> explain select * from one where username='abgvwfnt' and last_login='1338252525'; 
    +----+-------------+-------+------+---------------+----------+---------+-------+------+-------------+ 
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | 
    +----+-------------+-------+------+---------------+----------+---------+-------+------+-------------+ 
    | 1 | SIMPLE | one | ref | username | username | 24 | const |5 | Using where | 
    +----+-------------+-------+------+---------------+----------+---------+-------+------+-------------+ 
    1 row in set (0.00 sec) 
    注意這裏的key_len=24=8*3(8是username的長度,3是utf8編碼),rows=5,和下面一條sql語句搜索出來一樣

    mysql> select * from one where username='abgvwfnt'; 
    +-------+----------+----------+-------+------------+ 
    | id | username | password | level | last_login | 
    +-------+----------+----------+-------+------------+ 
    | 3597 | abgvwfnt | 234567 | 0 | 1338251420 | 
    | 7693 | abgvwfnt | 456789 | 0 | 1338251717 | 
    | 11789 | abgvwfnt | 456789 | 0 | 1338251992 | 
    | 15885 | abgvwfnt | 456789 | 0 | 1338252258 | 
    | 19981 | abgvwfnt | 456789 | 0 | 1338252525 | 
    +-------+----------+----------+-------+------------+ 
    5 rows in set (0.00 sec)

    mysql> select * from one where username='abgvwfnt' and last_login='1338252525'; 
    +-------+----------+----------+-------+------------+ 
    | id | username | password | level | last_login | 
    +-------+----------+----------+-------+------------+ 
    | 19981 | abgvwfnt | 456789 | 0 | 1338252525 | 
    +-------+----------+----------+-------+------------+ 
    1 row in set (0.00 sec) 
    這個就是要的返回結果,所以可以知道斷層(username,last_login),這樣只用到username索引,把用到索引的數據再重新檢查 last_login條件,這個相對全表查詢來說還是有性能上優化,這也是很多sql優化文章中提到的where 範圍查詢要放在最後 (這不絕對,但可以利用一部分索引)

  • 如果一個查詢where子句中確實不需要password列,那就用“補洞”。

    mysql> select distinct(password) from one; 
    +----------+ 
    | password | 
    +----------+ 
    | 234567 | 
    | 345678 | 
    | 456789 | 
    | 123456 | 
    +----------+ 
    4 rows in set (0.08 sec)

    可以看出password列中只有這幾個值,當然在現實中不可能密碼有這麼多一樣的,再說數據也可能不斷更新,這裏只是舉例說明補 洞的方法

    mysql> explain select * from one where username='abgvwfnt' and password in('123456','234567','345678','456789') and last_login='1338251170'; 
    +----+-------------+-------+-------+---------------+----------+---------+------+------+-------------+ 
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | 
    +----+-------------+-------+-------+---------------+----------+---------+------+------+-------------+ 
    | 1 | SIMPLE | one | range | username | username| 83 | NULL |4 | Using where | 
    +----+-------------+-------+-------+---------------+----------+---------+------+------+-------------+ 
    1 row in set (0.00 sec) 
    可以看出ref=83 所有的索引都用到了,type=range是因爲用了in子句。 這個被“補洞”列中的值應該是有限的,可預知的,如性別,其值只有男和女(加多一個不男不女也無妨)。 “補洞”方法也有瓶頸,當很多列,且需要補洞的相應列(可以多列)的值雖有限但很多(如中國城市)的時候,優化器在優化時組合 起來的數量是很大,這樣的話就要做好基準測試和性能分析,權衡得失,取得一個合理的優化方法。

  • like

    mysql> explain select * from one where username like 'abgvwfnt%'; 
    +----+-------------+-------+-------+---------------+----------+---------+------+------+-------------+ 
    | id | select_type | table | type | possible_keys | key | key_len | ref | 
    rows | Extra | 
    +----+-------------+-------+-------+---------------+----------+---------+------+------+-------------+ 
    | 1 | SIMPLE | one | range | username | username | 24 | NULL | 5 | Using where | 
    +----+-------------+-------+-------+---------------+----------+---------+------+------+-------------+ 
    1 row in set (0.00 sec)

    mysql> explain select * from one where username like '%abgvwfnt%'; 
    +----+-------------+-------+------+---------------+------+---------+------+-------+-------------+ 
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows| Extra | 
    +----+-------------+-------+------+---------------+------+---------+------+-------+-------------+ 
    | 1 | SIMPLE | one | ALL | NULL | NULL | NULL | NULL | 20259 | Using where | 
    +----+-------------+-------+------+---------------+------+---------+------+-------+-------------+ 
    1 row in set (0.01 sec) 
    對比就知道like操作abgvwfnt%能用到索引,%abgvwfnt%用不到

Order by 優化

  • filesort優化算法

    在mysql version()<4.1之前,優化器採用的是filesort第一種優化算法,先提取鍵值和指針,排序後再去提取數據,前後要搜索數據 兩次,第一次若能使用索引則使用,第二次是隨機讀(當然不同引擎也不同)。mysql version()>=4.1,更新了一個新算法,就是在第 一次讀的時候也把selcet的列也讀出來,然後在sort_buffer_size中排序(不夠大則建臨時表保存排序順序),這算法只需要一次讀 取數據。所以有這個廣爲人傳的一個優化方法,那就是增大sort_buffer_size。Filesort第二種算法要用到更多空間, sort_buffer_size不夠大反而會影響速度,所以mysql開發團隊定了個變量max_length_for_sort_data,當算法中讀出來的需要列 的數據的大小超過該變量的值才使用,所以一般性能分析的時候會嘗試把max_length_for_sort_data改小。

  • 單獨order by 用不了索引,索引考慮加where 或加limit

    先建一個索引(last_login),建的過程就不給出了

    mysql> explain select * from one order by last_login desc; 
    +----+-------------+-------+------+---------------+------+---------+------+-------+----------------+
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows 
    | Extra | 
    +----+-------------+-------+------+---------------+------+---------+------+-------+----------------+
    | 1 | SIMPLE | one | ALL | NULL | NULL | NULL | NULL | 2046 
    3 | Using filesort | 
    +----+-------------+-------+------+---------------+------+---------+------+-------+----------------+
    1 row in set (0.00 sec)

    mysql> explain select * from one order by last_login desc limit 10; 
    +----+-------------+-------+-------+---------------+------------+---------+------+------+-------+ 
    | id | select_type | table | type | possible_keys | key | key_len | ref 
    | rows | Extra | 
    +----+-------------+-------+-------+---------------+------------+---------+------+------+-------+ 
    | 1 | SIMPLE | one | index | NULL | last_login | 4 | NULL 
    | 10 | | 
    +----+-------------+-------+-------+---------------+------------+---------+------+------+-------+ 
    1 row in set (0.00 sec) 
    開始沒limit查詢是遍歷表的,加了limit後,索引可以使用,看key_len 和key

  • where + orerby 類型,where滿足最左前綴原則,且orderby的列和where子句用到的索引的列的子集。即是(a,b,c)索引, where滿足最左前綴原則且order by中列a、b、c的任意組合

    mysql> explain select * from one where username='abgvwfnt' and password ='123456 
    ' and last_login='1338251001' order by password desc,last_login desc; 
    +----+-------------+-------+------+---------------+----------+---------+-------------------+------+-------------+ 
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | 
    +----+-------------+-------+------+---------------+----------+---------+-------------------+------+-------------+ 
    | 1 | SIMPLE | one | ref | username | username | 83 | const,const,const | 1 | Using where | 
    +----+-------------+-------+------+---------------+----------+---------+-------------------+------+-------------+ 
    1 row in set (0.00 sec)

    mysql> explain select * from one where username='abgvwfnt' and password ='123456 
    ' and last_login='1338251001' order by password desc,level desc; 
    +----+-------------+-------+------+---------------+----------+---------+-------------------+------+----------------------------+ 
    | id | select_type | table | type | possible_keys | key | key_len | ref| rows | Extra | 
    +----+-------------+-------+------+---------------+----------+---------+-------------------+------+-----------------------------+ 
    | 1 | SIMPLE | one | ref | username | username | 83 | const,const,const | 1 | Using where; Using filesort | 
    +----+-------------+-------+------+---------------+----------+---------+-------------------+------+-----------------------------+

    1 row in set (0.00 sec) 
    上面兩條語句明顯的區別是多了一個非索引列level的排序,在extra這列對了Using filesort。 筆者測試結果:where滿足最左前綴且order by中的列是該多列索引的子集時(也就是說orerby中沒最左前綴原則限制),不管是否 有asc ,desc混合出現,都能用索引來滿足order by。因爲篇幅比較大,這裏就不一一列出。

    Ps:很優化博文都說order by中的列要where中出現的列(是索引)的順序一致,筆者認爲不夠嚴謹。

  • where + orerby+limit

    這個其實也差不多,只要where最左前綴,orderby也正確,limit在此影響不大

如何考慮order by來建索引

這個迴歸到創建索引的問題來,在比較常用的oder by的列和where中常用的列建立多列索引,這樣優化起來的廣度和擴張性都比較好, 當然如果要考慮UNION、JOIN、COUNT、IN等進來就複雜很多了

隔離列

隔離列是隻查詢語句中把索引列隔離出來,也就是說不能在語句中把列包含進表達式中,如id+1=2、inet_aton('210.38.196.138')--- ip轉換成整數、convert(123,char(3))---數字轉換成字符串、date函數等mysql內置的大多函數。

非隔離列影響性能很大甚至是致命的,這也就是趕集網石展的《三十六軍規》中的一條,雖然他沒說明是隔離列。 以下就測試一下:

首先建立一個索引(last_login ),這裏就不給出建立的代碼了,且把last_login改成整型(這裏只是爲了方便測試,並不是影響條件)

mysql> explain select * from one where last_login = 8388605;  
+----+-------------+-------+------+---------------+------------+---------+-------+-------+-------------+  
| id | select_type | table | type | possible_keys | key        | key_len | ref | rows  | Extra       |  
+----+-------------+-------+------+---------------+------------+---------+-------+-------+-------------+  
|  1 | SIMPLE      | one   | ref  | last_login    | last_login | 3       | const   | 1 | Using where |  
+----+-------------+-------+------+---------------+------------+---------+-------+-------+-------------+  
1 row in set, 1 warning (0.00 sec)  
容易看出建的索引已起效

mysql> explain select * from one where last_login +1= 8388606 ;  
+----+-------------+-------+------+---------------+------+---------+------+-------+-------------+  
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows    | Extra       |  
+----+-------------+-------+------+---------------+------+---------+------+-------+-------------+  
|  1 | SIMPLE      | one   | ALL  | NULL          | NULL | NULL    | NULL | 2049  
7 | Using where |  
+----+-------------+-------+------+---------------+------+---------+------+-------+-------------+  
1 row in set (0.00 sec)  
last_login +1=8388608非隔離列的出現導致查找的列20197,說明是遍歷整張表且索引不能使用。
這是因爲這條語句要找出所有last_login的數據,然後+1再和20197比較,優化器在這方面比較差,性能很差。
所以要儘可能的把列隔離出來,如last_login +1= 8388606改成login_login=8388607,或者把計算、轉換等操作先用php函數處理
過再傳遞給mysql服務器

OR、IN、UNION ALL,可以嘗試用UNION ALL

  • or會遍歷表就算有索引

    mysql> explain select * from one where username = 'abgvwfnt' or password='123456'; 
    +----+-------------+-------+------+---------------+------+---------+------+-------+-------------+ 
    | id | select_type | table | type | possible_keys | key | key_len | ref | rows| Extra | 
    +----+-------------+-------+------+---------------+------+---------+------+-------+-------------+ 
    | 1 | SIMPLE | one | ALL | username | NULL | NULL | NULL | 20259 | Using where | 
    +----+-------------+-------+------+---------------+------+---------+------+-------+-------------+ 
    1 row in set (0.00 sec)

  • 對於in,這個是有爭議的,網上很多優化方案中都提到儘量少用in,這不全面,其實在in裏面如果是常量的話,可一大膽的用in, 這個也是趕集網石展、阿里hellodab的觀點(筆者從微博中獲知)。應用hellodab一句話“MySQL用IN效率不好,通常是指in中 嵌套一個子查詢,因爲MySQL的查詢重寫可能會產生一個不好的執行計劃,而如果in裏面是常量的話,我認爲性能沒有任何問題, 可以放心使用”---------當然對於這個比較的話,沒有實戰數據的話很難辯解,就算有,影響性能的因素也很多,也許會每個 dba都有不同的測試結果.這也簽名最左前綴中“補洞”一個方法

  • UNION All 直接返回並集,可以避免去重的開銷。之所說“嘗試”用UNION All 替代 OR來優化sql語句,因爲這不是一直能優化的了, 這裏只是作爲一個方法去嘗試。

索引選擇性

索引選擇性是不重複的索引值也叫基數(cardinality)表中數據行數的比值,索引選擇性=基數/數據行,基數可以通過 “show index from 表名”查看。高索引選擇性的好處就是mysql查找匹配的時候可以過濾更多的行,唯一索引的選擇性最佳,值爲1。 那麼對於非唯一索引或者說要被創建索引的列的數據內容很長,那就要選擇索引前綴。這裏就簡單說明一下:

mysql> select count(distinct(username))/count(*)  from one;  
+------------------------------------+  
| count(distinct(username))/count(*) |  
+------------------------------------+  
|                             0.2047 |  
+------------------------------------+  
1 row in set (0.09 sec)  

count(distinct(username))/count( )就是索引選擇性,這裏0.2太小了。假如username列數據很長,則可以通過 select count(distinct(concat(first_name, left(last_name, N))/count( ) from one;測試出接近1的索引選擇性, 其中N是索引的長度,窮舉法去找出N的值,然後再建索引。

重複或多餘索引

很多phper開始都以爲建索引相對多點性能就好點,壓根沒考慮到有些索引是重複的,比如建一個(username),(username,password), (username,password,last_login),很明顯第一個索引是重複的,因爲後兩者都能滿足其功能。要有個意識就是,在滿足功能需求的 情況下建最少索引。對於INNODB引擎的索引來說,每次修改數據都要把主鍵索引,輔助索引中相應索引值修改,這可能會出現大量數 據遷移,分頁,以及碎片的出現。

系統配置與維護優化

重要的一些變量

  • key_buffer_size索引塊緩存區大小, 針對MyISAM存儲引擎,該值越大,性能越好.但是超過操作系統能承受的最大值,反而會使mysql變得不穩定. ----這是很重要的參數
  • sort_buffer_size 這是索引在排序緩衝區大小,若排序數據大小超過該值,則創建臨時文件,注意和MyISAM_sort_buffer_size的區別----這是很重要的參數
  • read_rnd_buffer_size當排序後按排序後的順序讀取行時,則通過該緩衝區讀取行,避免搜索硬盤。將該變量設置爲較大的值可以大大改進ORDER BY的性能。但是,這是爲每個客戶端分配的緩衝區,因此你不應將全局變量設置爲較大的值。相反,只爲需要運行大查詢的客戶端更改會話變量
  • join_buffer_size用於表間關聯(join)的緩存大小
  • tmp_table_size緩存表的大小
  • table_cache允許 MySQL 打開的表的最大個數,並且這些都cache在內存中
  • delay_key_write針對MyISAM存儲引擎,延遲更新索引.意思是說,update記錄時,先將數據up到磁盤,但不up索引,將索引存在內存裏,當表關閉時,將內存索引,寫到磁盤

更多參數查看: http://www.phpben.com/?post=70

optimize、Analyze、check、repair維護操作

  • optimize 數據在插入,更新,刪除的時候難免一些數據遷移,分頁,之後就出現一些碎片,久而久之碎片積累起來影響性能, 這就需要DBA定期的優化數據庫減少碎片,這就通過optimize命令。如對MyISAM表操作:optimize table 表名

    對於InnoDB表是不支持optimize操作,否則提示“Table does not support optimize, doing recreate + analyze instead”, 當然也可以通過命令:alter table one type=innodb; 來替代。

  • Analyze 用來分析和存儲表的關鍵字的分佈,使得系統獲得準確的統計信息,影響 SQL 的執行計劃的生成。對於數據基本沒有發生 變化的表,是不需要經常進行表分析的。但是如果表的數據量變化很明顯,用戶感覺實際的執行計劃和預期的執行計劃不 同的時候, 執行一次表分析可能有助於產生預期的執行計劃。Analyze table 表名
  • Check檢查表或者視圖是否存在錯誤,對 MyISAM 和 InnoDB 存儲引擎的表有作用。對於 MyISAM 存儲引擎的表進行表檢查, 也會同時更新關鍵字統計數據
  • Repair optimize需要有足夠的硬盤空間,否則可能會破壞表,導致不能操作,那就要用上repair,注意INNODB不支持repair操作

以上的操作出現的都是如下這是check

+----------+-------+--------------+-------------+  
| Table  | Op  | Msg_type| Msg_text |  
+----------+-------+--------------+-------------+  
| test.one | check | status  | OK     |  
+----------+-------+--------------+-------------+  

其中op是option 可以是repair check optimize,msg_type 表示信息類型,msg_text 表示信息類型,這裏就說明表的狀態正常。 如在innodb表使用repair就出現note | The storage engine for the table doesn't support repair

注意:以上操作最好在數據庫訪問量最低的時候操作,因爲涉及到很多表鎖定,掃描,數據遷移等操作,否則可能導致一些功能無法 正常使用甚至數據庫崩潰。

表結構的更新與維護

  • 改表結構。當要在數據量千萬級的數據表中使用alter更改表結構的時候,這是一個棘手問題。一種方法是在低併發低訪問量的時 候用平常的alter更改表。另外一種就是建另一個與要修改的表,這個表除了要修改的結構屬性外其他的和原表一模一樣,這樣就 能得到一個相應的.frm文件,然後用flush with read lock 鎖定讀,然後覆蓋用新建的.frm文件覆蓋原表的.frm, 最後unlock table 釋放表。
  • 建立新的索引。一般方法這裏不說。
    • 創建沒索引的a表,導入數據形成.MYD文件。
    • 創建包括索引b表,形成.FRM和.MYI文件
    • 鎖定讀寫
    • 把b表的.FRM和.MYI文件改成a表名字
    • 解鎖
    • 用repair創建索引。

    這個方法對於大表也是很有效的。這也是爲什麼很多dba堅持說“先導數據庫在建索引,這樣效率更快”

  • 定期檢查mysql服務器 定期使用show status、show processlist等命令檢查數據庫。這裏就不細說,這說起來也篇幅是比較大的,筆者對這個也不是很瞭解

第四部分:圖說mysql查詢執行流程

1. 查詢緩存,判斷sql語句是否完全匹配,再判斷是否有權限,兩個判斷爲假則到解析器解析語句,爲真則提取數據結果返回給用戶。 2. 解析器解析。解析器先詞法分析,語法分析,檢查錯誤比如引號有沒閉合等,然後生成解析樹。 3. 預處理。預處理解決解析器無法決解的語義,如檢查表和列是否存在,別名是否有錯,生成新的解析樹。 4. 優化器做大量的優化操作。 5. 生成執行計劃。 6. 查詢執行引擎,負責調度引擎獲取相應數據 7. 返回結果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章