1. 程式人生 > 實用技巧 ><MySQL>索引原理

<MySQL>索引原理

  • 索引介紹

    • 需求:一般的應用系統,讀寫比例大概在10:1左右,急需優化讀操作,MySQL裡的讀操作表現形式就是查詢語句,優化查詢語句就是目的。
    • 索引:相當於圖書的目錄,可以幫助使用者快速的找到需要的內容。
    • 本質:通過不斷地縮小想要獲取資料的範圍來篩選出最終想要的結果,同時把隨機的事件變成順序的事件,也就是說,有了這種索引機制,我們可以總是用同一種查詢方式來鎖定資料。
  • 索引方法 

    • B+樹索引:由平衡樹二叉查詢樹結合產生的一種平衡查詢樹。
      • 特點:所有的記錄節點都是按鍵值大小順序存放在同一層的葉節點中,葉節點間用指標相連,構成雙向迴圈連結串列,非葉節點(根節點、枝節點)只存放鍵值,不存放實際資料。
      • 例子
      • 查詢過程:
        • 小知識:系統從磁碟讀取資料到記憶體時是以磁碟塊(block)為基本單位的,位於同一磁碟塊中的資料會被一次性讀取出來,而不是按需讀取。InnoDB 儲存引擎使用頁作為資料讀取單位,頁是其磁碟管理的最小單位,預設 page 大小是 16kB。
        • 目標:查詢數字30
          • 1.首先會把磁碟塊1由磁碟載入到記憶體,此時發生一次IO,在記憶體中用二分查詢確定30在28和65之間,鎖定磁碟塊1的P2指標,記憶體時間非常短(相比磁碟的IO)可以忽略不計。
          • 2.通過磁碟塊1的P2指標的磁碟地址把磁碟塊由磁碟載入到記憶體,發生第二次IO,30在28和35之間,鎖定當前磁碟塊的P1指標。
          • 3.通過指標載入磁碟塊到記憶體,發生第三次IO,同時記憶體中做二分查詢找到30,結束查詢,總計三次IO。   
        • 真實的情況是,3層的b+樹可以表示上百萬的資料,如果上百萬的資料查詢只需要三次IO,效能提高將是巨大的,如果沒有索引,每個資料項都要發生一次IO,那麼總共需要百萬次的IO,顯然成本非常非常高。
    • HASH 索引
      • hash就是特殊形式的鍵值對,允許多個key對應相同的value,但不允許一個key對應多個value。
      • 索引建立方式
        • 為某一列或幾列建立hash索引,就會利用這一列或幾列的值通過一定的演算法計算出一個hash值,對應一行或幾行資料.
        • hash索引可以一次定位,不需要像樹形索引那樣逐層查詢,因此具有極高的效率.
  • 索引型別 

    • 普通索引
      • 功能:加速查詢
      • 建立表,表中新增索引
        • #建立表同時新增name欄位為普通索引
          create table tb1(
             id int not null auto_increment primary key,
             name varchar(100) not null,
             index idx_name(name)  
          );
      • 單獨建立索引
        • create index idx_name on tb1(name);
      • 刪除索引
        • drop index idx_name on tb1;
          
      • 檢視索引 
        • show index from tb1;
    • 唯一索引
      • 功能:加速查詢 和 唯一約束(不可含null)
      • 表中建立唯一索引
        • create table tb2(
            id int not null auto_increment primary key,
            name varchar(50) not null,
            age int not null,
            unique index idx_age (age)   
          )
      • 語句建立唯一索引
        • create unique index idx_age on tb2(age);
    • 主鍵索引:一個表中最多隻能有一個主鍵索引
      • 功能:加速查詢 和 唯一約束(不可含null) 
      • 表中建立主鍵
        • #方式一:
          create table tb3(
             id int not null auto_increment primary key,
             name varchar(50) not null,
             age int default 0 
          );
          
          #方式二:
          create table tb3(
             id int not null auto_increment,
             name varchar(50) not null,
             age int default 0 ,
             primary key(id)
          );
      • 單獨建立
        • alter table tb3 add primary key(id);
      • 刪除主鍵
        • #方式一
          alter table tb3 drop primary key;
          
          #方式二:
          #如果當前主鍵為自增主鍵,則不能直接刪除.需要先修改自增屬性,再刪除
          
          alter table tb3 modify id int ,drop primary key;
    • 組合索引:組合索引是將n個列組合成一個索引
      • 應用場景::頻繁的同時使用n列來進行查詢,如:where n1 = 'alex' and n2 = 666。
      • 表中建立組合索引
        • create table tb4(
            id int not null ,
            name varchar(50) not null,
            age int not null,
            index idx_name_age (name,age)   
          )
      • 單獨建立
        • create index idx_name_age on tb4(name,age);
    • 索引應用場景
      • 舉個例子來說,比如你在為某商場做一個會員卡的系統。
        
        這個系統有一個會員表
        有下列欄位:
        會員編號 INT
        會員姓名 VARCHAR(10)
        會員身份證號碼 VARCHAR(18)
        會員電話 VARCHAR(10)
        會員住址 VARCHAR(50)
        會員備註資訊 TEXT
        
        那麼這個 會員編號,作為主鍵,使用 PRIMARY
        會員姓名 如果要建索引的話,那麼就是普通的 INDEX
        會員身份證號碼 如果要建索引的話,那麼可以選擇 UNIQUE (唯一的,不允許重複)
        
  • 聚合索引和輔助索引

    • 資料庫中的B+樹索引可以分為聚集索引和輔助索引. 

      • 聚集索引:表中資料按主鍵B+樹存放,葉子節點直接存放整條資料,每張表只能有一個聚集索引。

        • 當你定義一個主鍵時,InnnodDB儲存引擎則把它當做聚集索引。

        • 如果你沒有定義一個主鍵,則InnoDB定位到第一個唯一索引,且該索引的所有列值均飛空的,則將其當做聚集索引。

        • 如果表沒有主鍵或合適的唯一索引INNODB會產生一個隱藏的行ID值6位元組的行ID聚集索引。 

      • 輔助索引:(也稱非聚集索引)是指葉節點不包含行的全部資料,葉節點除了包含鍵值之外,還包含一個書籤連線,通過該書籤再去找相應的行資料。

    • 何時使用聚集索引或非聚集索引

  • 測試索引  

    1. 建立表

      • CREATE TABLE userInfo(
            id int NOT NULL,
            name VARCHAR(16) DEFAULT NULL,
            age int,
            sex char(1) not null,
            email varchar(64) default null
        )ENGINE=MYISAM DEFAULT CHARSET=utf8;
    2. 建立儲存過程,插入資料

      • delimiter$$
        CREATE PROCEDURE insert_user_info(IN num INT)
        BEGIN
            DECLARE val INT DEFAULT 0;
            DECLARE n INT DEFAULT 1;
            -- 迴圈進行資料插入
            WHILE n <= num DO
                set val = rand()*50;
                INSERT INTO userInfo(id,name,age,sex,email)values(n,concat('alex',val),rand()*50,if(val%2=0,'女','男'),concat('alex',n,'@qq.com'));
                set n=n+1;
            end while;
        END $$
        delimiter;
    3. 呼叫儲存過程,插入500萬條資料

      • call insert_user_info(5000000);
        
    4. 修改引擎為INNODB

      • ALTER TABLE userinfo ENGINE=INNODB;
    5. 測試索引

      1. 沒索引查詢速度

        • SELECT * FROM userinfo WHERE id = 4567890;
        • 注意:無索引情況,mysql根本就不知道id等於4567890的記錄在哪裡,只能把資料表從頭到尾掃描一遍,此時有多少個磁碟塊就需要進行多少IO操作,所以查詢速度很慢.

      2. 在表中已經存在大量資料的前提下,為某個欄位段建立索引,建立速度會很慢  
        • CREATE INDEX idx_id on userinfo(id);
      3. 在索引建立完畢後,以該欄位為查詢條件時,查詢速度提升明顯
        • select * from userinfo where id  = 4567890;
      4. 注意
        • 1.mysql先去索引表裡根據b+樹的搜尋原理很快搜索到id為4567890的資料,IO大大降低,因而速度明顯提升

          2.我們可以去mysql的data目錄下找到該表,可以看到新增索引後該表佔用的硬碟空間多了 

          3.如果使用沒有新增索引的欄位進行條件查詢,速度依舊會很慢(如圖:)

  • 正確使用索引 

    • 資料庫表中新增索引後確實會讓查詢速度起飛,但前提必須是正確的使用索引來查詢,如果以錯誤的方式使用,則即使建立索引也會不奏效。即使建立索引,索引也不會生效:  

    • #1. 範圍查詢(>、>=、<、<=、!= 、between...and)
          #1. = 等號
          select count(*) from userinfo where id = 1000 -- 執行索引,索引效率高
          
          #2. > >= < <= between...and 區間查詢
          select count(*) from userinfo where id <100; -- 執行索引,區間範圍越小,索引效率越高
          
          select count(*) from userinfo where id >100; -- 執行索引,區間範圍越大,索引效率越低
          
          select count(*) from userinfo where id between 10 and 500000; -- 執行索引,區間範圍越大,索引效率越低
          
         #3. != 不等於
         select count(*) from userinfo where id != 1000;  -- 索引範圍大,索引效率低
         
         
      #2.like '%xx%'
          #為 name 欄位新增索引
          create index idx_name on userinfo(name);
          
          select count(*) from userinfo where name like '%xxxx%'; -- 全模糊查詢,索引效率低
          select count(*) from userinfo where name like '%xxxx';   -- 以什麼結尾模糊查詢,索引效率低
        
          #例外: 當like使用以什麼開頭會索引使用率高
          select * from userinfo where name like 'xxxx%'; 
      
      #3. or 
          select count(*) from userinfo where id = 12334 or email ='xxxx'; -- email不是索引欄位,索引此查詢全表掃描
          
          #例外:當or條件中有未建立索引的列才失效,以下會走索引
          select count(*) from userinfo where id = 12334 or name = 'alex3'; -- id 和 name 都為索引欄位時, or條件也會執行索引
      
      #4.使用函式
          select count(*) from userinfo where reverse(name) = '5xela'; -- name索引欄位,使用函式時,索引失效
          
          #例外:索引欄位對應的值可以使用函式,我們可以改為一下形式
          select count(*) from userinfo where name = reverse('5xela');
      
      #5.型別不一致
          #如果列是字串型別,傳入條件是必須用引號引起來,不然...
          select count(*) from userinfo where name = 454;
              
          #型別一致
          select count(*) from userinfo where name = '454';
      
      #6.order by
          #排序條件為索引,則select欄位必須也是索引欄位,否則無法命中  
          select email from userinfo ORDER BY name DESC; -- 無法命中索引
      
          select name from userinfo ORDER BY name DESC;  -- 命中索引
              
          #特別的:如果對主鍵排序,則還是速度很快:
          select id from userinfo order by id desc;
  • 注意事項

    • 1. 避免使用select *
      2. 其他資料庫中使用count(1)或count(列) 代替 count(*),而mysql資料庫中count(*)經過優化後,效率與前兩種基本一樣.
      3. 建立表時儘量時 char 代替 varchar
      4. 表的欄位順序固定長度的欄位優先
      5. 組合索引代替多個單列索引(經常使用多個條件查詢時)
      6. 使用連線(JOIN)來代替子查詢(Sub-Queries)
      7. 不要有超過4個以上的表連線(JOIN)
      8. 優先執行那些能夠大量減少結果的連線。
      9. 連表時注意條件型別需一致
      10.索引雜湊值不適合建索引,例:性別不適合
  • 大資料分頁優化  

    • 大資料分頁

      • select * from userinfo limit 3000000,10;
    • 優化方案:

      • 一. 簡單粗暴,就是不允許檢視這麼靠後的資料,比如百度就是這樣的

        • 最多翻到72頁就不讓你翻了,這種方式就是從業務上解決;

      • 二.在查詢下一頁時把上一頁的行id作為引數傳遞給客戶端程式,然後sql就改成了

        • select * from userinfo where id>3000000 limit 10;  
        • 這條語句執行也是在毫秒級完成的,id>300w其實就是讓mysql直接跳到這裡了,不用依次在掃描全面所有的行。

        • 如果你的table的主鍵id是自增的,並且中間沒有刪除和斷點,那麼還有一種方式,比如100頁的10條資料

          • select * from userinfo where id>100*10 limit 10;
      • 三.最後第三種方法:延遲關聯

        • 我們在來分析一下這條語句為什麼慢,慢在哪裡。

          • select * from userinfo limit 3000000,10;
        • 玄機就處在這個 * 裡面,這個表除了id主鍵肯定還有其他欄位 比如 name age 之類的,因為select * 所以mysql在沿著id主鍵走的時候要回行拿資料,走一下拿一下資料;

        • 如果把語句改成 :

          • select id from userinfo limit 3000000,10;
        • 你會發現時間縮短了一半;然後我們在拿id分別去取10條資料就行了;

        • 語句就改成這樣了:

          • select table.* from userinfo inner join ( select id from userinfo limit 3000000,10 ) as tmp on tmp.id=userinfo.id;
      • 這三種方法最先考慮第一種 其次第二種,第三種是別無選擇