上期回顧:http://www.javashuo.com/article/p-rkyvbstc-q.htmlhtml
本節腳本:https://github.com/lotapp/BaseCode/blob/master/database/SQL/02.索引、查詢優化.sqlmysql
文章有點小長,但認真閱讀確定會有所感觸和收穫的。PS:我把我能想到的都列下來了,若是有新的會追加,歡迎補充和糾錯~git
大方向:減小冗餘索引,避免重複(無用)索引github
大一統分類:算法
B+ Tree
索引、hash
索引(鍵值索引,只有Memory存儲引擎支持
)、R Tree
索引(空間索引,MyISAM存儲引擎支持
)、Fulltext
索引(全文索引)PS:索引一般作查詢條件的字段(索引是在存儲引擎級別實現的)sql
經常使用分類:數據庫
show index from tb_name;
create [unique] index index_name on tb_name(列名,...)
alter table tb_name add [unique] index [index_name] on (列名,...)
drop index index_name on tb_name
alter table tb_name drop index index_name
先回顧下上節課內容:app
手寫SQL的語法順序:運維
select distinct <select_list> from <tb_name> <join_type> join <right_table> on <join_condition> where <where_condition> group by <group_by_list> having <having_condition> order by <order_by_list> limit <limit_number>
SQL執行順序:函數
from <tb_name>
on <join_condition>
<join_type> join <right_table>
where <where_condition>
group by <group_by_list>
having <having_condition>
select [distinct] <select_list>
order by <order_by_list>
limit <limit_number>
語法:explain + SQL語句
執行計劃:使用explain
關鍵詞能夠模擬優化器執行SQL查詢語句,通常用來分析查詢語句或者表結構的性能瓶頸
執行計劃通常用來幹這些事情:
主要是看這幾個參數:
select_type
:查詢類型
subquery
:用於where中的子查詢(簡單子查詢)derived
:用於from中的子查詢union
:union語句的第一個以後的select語句union result
:匿名臨時表type
:訪問類型(MySQL查詢表中行的方式)
possible_keys
:查詢可能會用到的索引key
:查詢中使用了的索引key_len
:索引使用的字節數(詳解見附錄3)
ref
:顯示key列索引用到了哪些列、常量值
rows
:估算大概須要掃描多少行Extra
:額外信息(性能遞減)
using where
:在存儲引擎檢索後,再進行一次過濾order by|group by
後面的索引列不一致(只能用到一個索引)explain select * from users where id<10 order by email;
(只用到了id)select_type
:查詢類型
-- `subquery`:用於where中的子查詢(簡單子查詢) explain select name, age from students where age > (select avg(age) from students); -- `union`:union語句的第一個以後的select語句 -- `union result`:匿名臨時表 explain select name, age, work from students where name = '小張' union select name, age, work from students where name = '小明'; -- `derived`:用於from中的子查詢 explain select * from (select name, age, work from students where name = '小張' union select name, age, work from students where name = '小明') as tmp;
圖示輸出:
type
:訪問類型(MySQL查詢表中行的方式)
-- all:全表掃描(效率極低) explain select * from students where name like '%小%'; -- index:根據索引的次序進行全表掃描(效率低) explain select name, age, work from students where name like '%小%'; -- 其實就是上面全表掃描的改進版 -- range:根據索引作指定範圍掃描 explain select name, age, work from students where id > 5; -- ref:返回表中全部匹配某單個值的全部行 explain select name, age, work from students where name = '小明'; -- eq_ref:等同於ref,與某個值作比較且僅返回一行 explain select * from userinfo inner join (select id from userinfo limit 10000000,10) as tmp on userinfo.id = tmp.id; -- 1s -- const:根據具備惟一性索引查找時,且返回單個行(**性能最優**) explain select name, age, work from students where id = 3; -- 通常都是主鍵或者惟一鍵
圖示輸出:
latin1
|ISO8859
佔1個字節,gbk
佔2個字節,utf8
佔3個字節char(20) index 可空
key-len=20*3(utf8)+1(可空)=61
varchar(20) index 可空
key-len=20*3(utf8)+2(可變長度)+1(是否可空的標記)=63
create table if not exists `students` ( id int unsigned auto_increment primary key, name varchar(25) not null default '' comment '姓名', age tinyint unsigned not null default 0 comment '年齡', work varchar(20) not null default '普通學生' comment '職位', create_time datetime not null comment '入學時間', datastatus tinyint not null default 0 comment '數據狀態' ) charset utf8 comment '學生表'; -- select current_timestamp(), now(), unix_timestamp(); insert into students(name, age, work, create_time, datastatus) values ('111', 22, 'test', now(), 99), ('小張', 23, '英語課表明', now(), 1), ('小李', 25, '數學課表明', now(), 1), ('小明', 21, '普通學生', now(), 1), ('小潘', 27, '物理課表明', now(), 1), ('張小華', 22, '生物課表明', now(), 1), ('張小周', 22, '體育課表明', now(), 1), ('小羅', 22, '美術課表明', now(), 1); -- 建立一個組合索引 create index ix_students_name_age_work on students (name, age, work);
說了這麼多題外話,如今進入正題:
數值 > 日期 > char > varchar > text、blob
crc32
(用bigint存儲)索引空間就會小不少並且能夠避免全表掃描select crc32('http://www.baidu.com/shop/1.html');
CRC32碰撞後的解決方案
)
select xxx from urls where crc_url=563216577 and url='url地址'
PS:須要關注的技術點:crc32
項目裏面使用最多的是組合索引,這邊先以組合索引爲例:
-- 若是我查詢的時候,索引的三列都用到了,那麼速度無疑是最快的 -- Extra:using where explain select id, name, age, work, create_time from students where name = '小張' and age = 23 and work = '英語課表明'; -- PS:★儘可能使用覆蓋索引★(近乎萬能) -- 覆蓋索引:僅僅查找索引就能找到所須要的數據 -- Extra:using where;using index explain select name, age, work from students where name = '小張' and age = 23 and work = '英語課表明'; -- PS:通常把常常select出的列設置一個組合索引,通常不超過5個
圖示:
類比火車,火車頭本身能夠開,車身要是沒有了車頭就開不了
-- 查詢的時候從最左邊的列開始,而且不跳過中間的列,一直到最後 explain select id, name, age, work, create_time from students where name = '小張' and age = 23 and work = '英語課表明'; -- 跳過了中間的age,這時候只用到了name列的索引(work列沒用到) explain select id, name, age, work, create_time from students where name = '小張' and work = '英語課表明';
圖示:
再看兩個補充案例:
-- PS:若是跳過了第一列,這時候索引一個也用不到,直接全表掃描了 explain select id, name, age, work, create_time from students where age = 23 and work = '英語課表明'; -- PS:列不必定須要按照指定順序來寫 explain select id, name, age, work, create_time from students where age = 23 and work = '英語課表明' and name = '小張';
圖示:
-- name、age、work索引生效時,key_len=140 explain select id, name, age, work, create_time, datastatus from students where name = '小張' and age = 23 and work = '英語課表明'; -- 如今key_len=78 ==> work列索引就失效了(PS:age索引列未失效,只是age以後的列失效了) explain select id, name, age, work, create_time, datastatus from students where name = '小張' and age > 22 and work = '英語課表明';
圖示:
補充說明:
-- 加快查詢速度可使用覆蓋索引 explain select name, age, work from students where name = '小張' and age > 22 and work = '英語課表明'; -- PS:多個主鍵列也同樣 explain select id, name, age, work from students where name = '小張' and age > 22 and work = '英語課表明'; -- PS:調換順序是無法解決範圍後面索引失效的(原本對順序就不在乎) explain select id, name, age, work, create_time, datastatus from students where name = '小張' and work = '英語課表明' and age > 22;
圖示:
容易致使全表掃描,這時候利用覆蓋索引能夠簡單優化下
!=
、is not null
、is null
、not in
、in
、like
慎用!=
、is not null
、is null
的案例
-- 1.不等於案例 -- 索引失效(key,key_len ==> null) explain select id, name, age, work, create_time, datastatus from students where name != '小明'; -- <> 等同於 != -- 項目裏面不少使用都要使用,那怎麼辦呢?==> 使用覆蓋索引 -- key=ix_students_name_age_work,key_len=140 explain select name, age, work from students where name != '小明'; -- <> 等同於 != -- 2.is null、is not null案例 -- 索引失效(key,key_len ==> null) explain select id, name, age, work, create_time, datastatus from students where name is not null; -- 解決:覆蓋索引 key=ix_students_name_age_work,key_len=140 explain select name, age, work from students where name is not null;
圖示:
not in
、in
的案例
-- 3.not in、in案例 -- 索引失效(key,key_len ==> null) explain select id, name, age, work, create_time, datastatus from students where name in ('小明', '小潘', '小李'); explain select id, name, age, work, create_time, datastatus from students where name not in ('小明', '小潘', '小李'); -- 解決:覆蓋索引 key=ix_students_name_age_work,key_len=140 explain select name, age, work from students where name in ('小明', '小潘', '小李'); explain select name, age, work from students where name not in ('小明', '小潘', '小李');
圖示:
like
案例:儘可能使用xxx%
的方式來全文搜索,能和覆蓋索引聯合使用更好
-- 4.like案例 -- 索引不失效 key=ix_students_name_age_work,key_len=77(儘可能這麼用like) explain select id, name, age, work, create_time, datastatus from students where name like '張%'; -- 索引失效 explain select id, name, age, work, create_time, datastatus from students where name like '%張'; -- 索引失效 explain select id, name, age, work, create_time, datastatus from students where name like '%張%'; -- 解決:覆蓋索引 key=ix_students_name_age_work,key_len=140(儘可能避免) explain select name, age, work from students where name like '%張%';
-- 4.2.計算、函數、類型轉換(自動 or 手動)【儘可能避免】 -- 這時候索引直接失效了,並全表掃描了 -- 解決雖然可使用覆蓋索引,可是儘可能避免下面的狀況: -- 1.計算 explain select id, name, age, work, create_time, datastatus from students where age = (10 + 13); -- 2.隱式類型轉換(111==>'111') explain select id, name, age, work, create_time, datastatus from students where name = 111; -- PS:字符類型不加引號索引就直接失效了 -- 雖然覆蓋索引能夠解決,可是不要這樣作(嚴格意義上講,這個算個錯誤) -- 3.函數 explain select id, name, age, work, create_time, datastatus from students where right(name, 1) = '明';
圖示:
光看沒意思,再舉個簡單的業務案例:
eg:用戶通常都是根據商品的大分類=>小分類=>品牌來查找,有時候到不看品牌,直接小分類後就本身找了。那麼組合索引能夠這麼建:
index(分類id,商品價格)
,index(分類id,品牌id,商品價格)
(通常都須要根據查詢日記來肯定)
PS:有些條例是流傳甚廣的,有些是工做中的經驗,至少都是我踩過坑的,能夠相對放心(業務不一樣優化角度不一樣)
-- 5.1.or改爲union -- 如今高版本對只有一個or的sql語句有了優化 explain select id, name, age, work, create_time, datastatus from students where name = '小明' or name = '小張' or name = '小潘'; -- PS:等同上面or的語句 explain select id, name, age, work, create_time, datastatus from students where name in ('小明', '小張', '小潘'); -- 高效 explain select id, name, age, work, create_time, datastatus from students where name = '小明' union all select id, name, age, work, create_time, datastatus from students where name = '小張' union all select id, name, age, work, create_time, datastatus from students where name = '小潘';
PS:union老是產生臨時表,優化起來比較棘手
通常來講union子句儘可能查詢最少的行,union子句在內存中合併結果集須要去重(浪費資源),因此使用union的時候儘可能加上all(在程序級別去重便可)
通常都是count(主鍵|索引)
,但如今count(*)
基本上數據庫內部都優化過了(根據公司要求使用便可)
PS:記得當時踩了次坑,等復現的時候補上案例(記得好像跟null相關)
看下就知道爲何說無所謂了(PS,你count(非索引)
就有所謂了)
explain select count(id) -- 經常使用 from userinfo; explain select count(*) from userinfo; -- 你`count(非索引)`就有所謂了 explain select count(password) from userinfo;
我想說的優化是下面這個count優化案例:(有時候拆分查詢會更快)
-- 須要統計id>10000的數據總量(實際中可能會根據時間來統計) explain select count(*) as count from userinfo where id > 10000; -- 2s -- 分解成用總數-小數據統計 ==> 1s explain select (select count(*) from userinfo) - (select count(*) from userinfo where id <= 10000) as count;
執行圖示:
分析圖示:
group by
和order by
的列儘可能相同,這樣能夠避免filesort
-- 5.3.group by和order by的列儘可能相同,這樣能夠避免filesort explain select * from students group by name order by work; explain select * from students group by name order by name; -- 加where條件也同樣 explain select * from students where name like '小%' group by age order by work; -- PS:通常group by和order by的列都和where索引列相同(不一致也只會使用一個索引) explain select * from students where name like '小%' and age>20 group by name order by name; -- where後面的索引列和`order by|group by`後面的索引列不一致 -- id和email都是索引,但只用了一個索引 explain select * from users where id < 10 order by email;
圖示:
PS:不一致也只會使用一個索引(在索引誤區有詳細說明)
通常來講都是用鏈接查詢來代替子查詢,有些時候子查詢更方便(具體看業務吧)
-- 用exists代替in?MySQL查詢優化器針對in作了優化(改爲了exists,當users表越大查詢速度越慢) explain select * from students where name in (select username from users where id < 7); -- ==> 等同於: explain select * from students where exists(select username from users where username = students.name and users.id < 7); -- 真正改進==>用鏈接查詢代替子查詢 explain select students.* from students inner join users on users.username = students.name and users.id < 7; -- 等效寫法:這個tmp是臨時表,是沒有索引的,若是須要排序能夠在()裏面先排完序 explain select students.* from students inner join (select username from users where id < 7) as tmp on students.name = tmp.username;
圖示:(內部已經把in轉換成exists了,因此改不改寫無所謂了)
limit offset,N
:mysql並非跳過offset
行,而後取N
行,而是取offset+N
行,而後放棄前offset
行,返回N
行
offset越大效率越低
(你去翻貼的時候,頁碼越大通常越慢)爲了更加的直觀,咱們引入一下profiling
-- 查看profiling系統變量 show variables like '%profil%'; -- profiling:開啓SQL語句剖析功能(開啓以後應爲ON) -- 來查看是否已經啓用profile select @@profiling; -- 啓動profile(當前會話啓動) set profiling = 1; -- 0:未啓動,1:啓動 show profiles; -- 顯示查詢的列表 show profile for query 5; -- 查看指定編號查詢的詳細信息
輸出:
MariaDB [dotnetcrazy]> show variables like '%profil%'; +------------------------+-------+ | Variable_name | Value | +------------------------+-------+ | have_profiling | YES | | profiling | OFF | | profiling_history_size | 15 | +------------------------+-------+ 3 rows in set (0.002 sec) MariaDB [dotnetcrazy]> select @@profiling; +-------------+ | @@profiling | +-------------+ | 0 | +-------------+ 1 row in set (0.000 sec) MariaDB [dotnetcrazy]> set profiling = 1; Query OK, 0 rows affected (0.000 sec)
上面設置完後,分別執行下面SQL:
select * from userinfo limit 10,10; select * from userinfo limit 1000,10; select * from userinfo limit 100000,10; select * from userinfo limit 1000000,10; select * from userinfo limit 10000000,10;
輸出:
+----------+------------+------------------------------------------+ | Query_ID | Duration | Query | +----------+------------+------------------------------------------+ | 1 | 0.00060250 | select * from userinfo limit 10,10 | | 2 | 0.00075870 | select * from userinfo limit 1000,10 | | 3 | 0.03121300 | select * from userinfo limit 100000,10 | | 4 | 0.30530230 | select * from userinfo limit 1000000,10 | | 5 | 3.03068020 | select * from userinfo limit 10000000,10 | +----------+------------+------------------------------------------+
圖示:
limit 5,3 ==> where id > 5 limit 3;
覆蓋索引+延遲關聯
:經過使用覆蓋索引查詢返回須要的主鍵,再根據主鍵關聯原表得到須要的數據
主鍵爲uuid
或id不連續
(eg:部分數據物理刪除了等等)說太空洞,演示下就清楚了:
-- 全表掃描 explain select * from userinfo limit 10000000,10; -- 3s -- 先range過濾了一部分 explain select * from userinfo where id > 10000000 limit 10; -- 20ms -- 內部查詢使用了索引覆蓋 explain select * from userinfo inner join (select id from userinfo limit 10000000,10) as tmp on userinfo.id = tmp.id; -- 2s
分析圖示:
查詢圖示:
不少人喜歡把where條件的經常使用列上都加上索引,可是遺憾的事情是:獨立的索引只能同時用上一個
組合索引
別不信,來驗證一下就知道了:
-- id和email都是索引,可是隻能使用一個索引(獨立的索引只能同時用上一個) -- id的key-len=4(int4個字節) -- email的key-len=152(50*3(utf8下每一個字符佔3位)+2(varchar須要額外兩個字節存放)==>152) -- 1.惟一索引和主鍵:優先使用主鍵 explain select * from users where id = 4 and email = 'xiaoming@qq.com'; -- 2.組合索引和主鍵:優先使用主鍵 explain select * from users where id=4 and createtime='2019-02-16 17:10:29'; -- 3.惟一索引和組合索引:優先使用惟一索引 explain select * from users where createtime='2019-02-16 17:10:29' and email='xiaoming@qq.com'; -- 4.組合索引和通常索引:優先使用組合索引 -- create index ix_users_datastatus on users(datastatus); -- create index ix_users_username_password on users(username,password); explain select * from users where datastatus=1 and username='小明'; -- 刪除臨時添加的索引 -- drop index ix_users_datastatus on users; -- drop index ix_users_username_password on users;
圖示:
PS:根據測試得知,一次只能使用1個索引。索引優先級:主鍵 > 惟一 > 組合 > 普通
舉個標籤表的例子:
create table tags ( id int unsigned auto_increment primary key, aid int unsigned not null, tag varchar(25) not null, datastatus tinyint not null default 0 ); insert into tags(aid,tag,datastatus) values (1,'Linux',1),(1,'MySQL',1),(1,'SQL',1),(2,'Linux',1),(2,'Python',1); select id, aid, tag, datastatus from tags;
輸出:
+----+-----+--------+------------+ | id | aid | tag | datastatus | +----+-----+--------+------------+ | 1 | 1 | MySQL | 1 | | 2 | 1 | SQL | 1 | | 3 | 2 | Linux | 1 | | 4 | 2 | Python | 1 | +----+-----+--------+------------+
實際應用中可能會根據tag查找文章列表
,也可能經過文章id查找對應的tag列表
項目裏面通常是這麼創建索引(冗餘索引):index(文章id,tag),index(tag,文章id),這樣在上面兩種狀況下能夠直接用到覆蓋索引
create index ix_tags_aid_tag on tags(aid,tag); create index ix_tags_tag_aid on tags(tag,aid); select tag from tags where aid=1; select aid from tags where tag='Linux';
這邊簡單說下,下一章應該還會繼續說運維相關的知識
數據庫表使用時間長了會出現碎片,能夠按期修復一下(不影響數據):optimize table users;
修復表的數據以及索引碎片會把數據文件整理一下,這個過程相對耗費時間(數據量大的狀況下)通常根據狀況選擇按周|月|年修復一下
PS:能夠配合crontab
(定時任務)使用:
crontab -e
:***** 命令 [ > /dev/null 2>&1 ]
5個*的含義
:分
、時
、日
、月
、周
>> /xx/日誌文件
:輸出重定向到日記文件(不包含錯誤信息)>> /xx/日誌文件 2>&1
:輸出信息包括錯誤信息> /dev/null 2>&1
:出錯信息重定向到垃圾桶(黑洞)21*** xxx
==> 天天 1:02 執行 xxx命令5921*** xxx
==> 天天 21::59 執行 xxx命令*/*1*** xxx
==> 每1小時 執行一次xxx命令
*/
開頭課後拓展:
【推薦】一步步分析爲何B+樹適合做爲索引的結構 https://blog.csdn.net/weixin_30531261/article/details/79312676 善用mysql中的FROM_UNIXTIME()函數和UNIX_TIMESTAMP()函數 https://www.cnblogs.com/haorenergou/p/7927591.html 【推薦】MySQL crc32 & crc64函數 提升字符串查詢效率 https://www.jianshu.com/p/af6cc7b72dac MySQL優化之profile https://www.cnblogs.com/lizhanwu/p/4191765.html