開發人員MySQL調優-實戰篇1-單_雙表查詢優化

建議先看看開發人員MySQL調優-實戰篇0sql

單表案例分析

準備數據腳本函數

DROP TABLE IF EXISTS `tb_tmp_vote`;
CREATE TEMPORARY TABLE `tb_tmp_vote` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `user_id` varchar(20) NOT NULL DEFAULT '' COMMENT '用戶Id',
    `vote_num` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '投票數',
    `group_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用戶組id 0-未激活用戶 1-普通用戶 2-vip用戶 3-管理員用戶',
    `status` tinyint(2) unsigned NOT NULL DEFAULT '1' COMMENT '狀態 1-正常 2-已刪除',
    `create_time` date DEFAULT '0000-00-00 00:00:00' COMMENT '建立時間',
    PRIMARY KEY (`id`)
)COMMENT='投票記錄表';
​
DROP TABLE IF EXISTS `tb_vote`;
CREATE TABLE `tb_vote` (
    `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `user_id` varchar(20) NOT NULL DEFAULT '' COMMENT '用戶Id',
    `vote_num` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '投票數',
    `group_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用戶組id 0-未激活用戶 1-普通用戶 2-vip用戶 3-管理員用戶',
    `status` tinyint(2) unsigned NOT NULL DEFAULT '1' COMMENT '狀態 1-正常 2-已刪除',
    `create_time` date DEFAULT '0000-00-00 00:00:00' COMMENT '建立時間',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='投票記錄表';
​
-- 建立生成長度爲n的隨機字符串的函數
DELIMITER // -- 修改MySQL delimiter:'//'
DROP FUNCTION IF EXISTS `rand_string` //
SET NAMES utf8 //
CREATE FUNCTION `rand_string` (n INT) RETURNS VARCHAR(255) CHARSET 'utf8'
BEGIN 
    DECLARE char_str varchar(100) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    DECLARE return_str varchar(255) DEFAULT '';
    DECLARE i INT DEFAULT 0;
    WHILE i < n DO
        SET return_str = concat(return_str, substring(char_str, FLOOR(1 + RAND()*62), 1));
        SET i = i+1;
    END WHILE;
    RETURN return_str;
END //
​
-- 建立插入數據的存儲過程
DROP PROCEDURE IF EXISTS `add_tb_vote_memory` //
CREATE PROCEDURE `add_tb_vote_memory`(IN n INT)
BEGIN
    DECLARE i INT DEFAULT 1;
    DECLARE vote_num INT DEFAULT 0;
    DECLARE group_id INT DEFAULT 0;
    DECLARE status TINYINT DEFAULT 1;
    WHILE i < n DO
        SET vote_num = FLOOR(1 + RAND() * 10000);
        SET group_id = FLOOR(0 + RAND()*3);
        SET status = FLOOR(1 + RAND()*2);
        INSERT INTO `tb_tmp_vote` VALUES (NULL, rand_string(20), vote_num, group_id, status, NOW());
        SET i = i + 1;
    END WHILE;
END //
DELIMITER ;  -- 改回默認的 MySQL delimiter:';'
​
call add_tb_vote_memory(500000);
​
INSERT INTO tb_vote SELECT * FROM `tb_tmp_vote`;

需求:你的項目經理說要實現一個查詢某一天投票數超過某一個值的用戶,並按用戶ID升序排列學習

#在這裏傳遞過來的值寫成了固定值,代碼裏面用綁定變量(佔位符)
select user_id,vote_num
  from tb_vote 
 where create_time = '2018-03-03' 
   and vote_num > 9000 
 order by user_id;

查看執行計劃優化

一眼看過去,type=ALL,extra中出現了Using filesort,這兩個是須要去掉的spa

看看sql語句中使用到的字段,先在create_time,vote_num,user_id上建立索引.net

create index idx_tb_vote_c_v_u on tb_vote(create_time,vote_num,user_id);

type=ALL變成了type=range,爲何喃?由於雖然走了索引掃描,可是條件有一個vote_num > 9000blog

雖然type=ALL優化掉了,可是Using filesort還在,爲何喃?索引雖然是有序的,可是若是隻按照user_id排序的話,MySQL對整個結果集的user_id,vote_num的順序與索引中默認的順序不一致,致使MySQL必須從新排序。我想能夠有兩個方法來解決整個問題排序

1.在order by 中添加vote_num字段索引

select user_id,vote_num
  from tb_vote 
 where create_time = '2018-03-03' 
   and vote_num > 9000 
 order by vote_num,user_id;

2.再建立一個索引ip

create index idx_tb_vote_c_u on tb_vote(create_time,user_id);

比較這兩種方案:

按照前面說的type=ref優於type=range,但讀取的rows的值ref卻比range多不少,由於MySQL會將全部create_time='2018-03-03'的數據行找出來比較是否vote_num>9000,因此最終ref還會比range慢。可是第一種方案和需求有一點變化,這種時候就須要和項目經理溝通,項目經理再和客戶溝通,若是對業務沒什麼影響,就能夠採起第一種方案

到這裏該SQL的問題算是解決了,實際生產中絕對沒有這麼簡單,由於一個表的字段可能會很是多,好比電信的詳單,有幾十個字段,可能涉及到的列有十多個,如何建立索引將會是一個很是複雜的問題,最好和DBA一塊兒商量着來(通常大系統都會有DBA按天監控一些重要表的DDL操做,按期收集表的統計信息,固化執行計劃等)

注意在寫SQL的時候,若是是字符類型的列,必定要加上單引號,不然生成執行計劃的時候會出問題,你認爲它會用到索引,它就不會讓你如願以償

兩張表案例分析

根據上面的表造一份user數據

create table tb_v_user
 select user_id,left(user_id,5) as user_name,floor(40+rand()*(20)) as age,floor(1+rand()*2) as gendor 
   from tb_vote;

這樣取值的話能夠user_name就會有重複,執行下面的語句拿到一些重複的user_name

select user_name,count(*) 
    from tb_v_user
 group by user_name 
having count(*)>5;
user_name count(*)
1F7sJ   6
2fXTq   6

刪除以前建立的索引,方便從頭開始分析

#使用了刪除索引的兩種寫法
alter table tb_vote drop index idx_tb_vote_c_v_u;
drop index idx_tb_vote_c_u on tb_vote;

 

需求1:查詢出user_name爲‘1F7sJ’的投票結果

select a.user_name,a.user_id,b.vote_num from tb_v_user a 
  join tb_vote b on a.user_id = b.user_id 
 where a.user_name = '1F7sJ';

如今兩張表在關聯的字段上面都沒有索引,MySQL首先加載tv_vote表,也就是關聯查詢右邊的表,意思就是先拿到tb_vote的全部數據和tb_v_user關聯,再判斷是否是1F7sJ用戶,兩張表都使用了ALL(全表掃描),而後在內存中對join。接下來嘗試在b表上建立索引。

create index idx_tb_vote_user_id on tb_vote(user_id);

能夠看到b表type=ref了,這結果很是好。MySQL估算出來b錶針對每一個a表的關聯只會撈一條數據出來。

咱們如今是在b表上建立的索引,若是剛纔是在a表上建立索引會是什麼效果喃?

drop index idx_tb_vote_user_id on tb_vote;
create index idx_tb_v_user_user_id on tb_v_user(user_id);

如今a\b兩張表的加載順序變了,a表的type=ref。能夠推斷出MySQL針對一樣的SQL,本身會作優化,並不會死板的認爲必定先加載關聯查詢右邊的表。

能夠設想另一種狀況,若是兩張表關聯,一張表很小,另一張表相對要大不少,按照上面的處理會是什麼樣的效果喃?有興趣的同窗能夠試試。按照上面學習的理論,我推測小表上建索引是沒有用的,小表必定會先加載做爲驅動表。

再來將兩張表的user_id都加上索引試試

create index idx_tb_vote_user_id on tb_vote(user_id);

然而並無什麼變化,爲何喃?由於做爲驅動的a表,要找到符合條件的‘1F7sJ’,依然須要全表掃描

那麼再到a表的user_name上建立一個索引

create index idx_tb_v_user_user_name on tb_v_user(user_name);

 

這個效果就很好了,全部type=ref,並且估算出來須要撈的數據總共才7行,很是快。

此時若是需求變了,還要對結果作一些排序怎麼辦喃,能夠參考單表優化的經驗試試

總結一下表關聯的狀況:找到驅動表,減小驅動表須要關聯數據行的代價,被關聯表的關聯字段必定要有索引

相關文章
相關標籤/搜索