[06][03][04] MySQL性能優化總結

優化思路

作為架構師或者開發人員,說到數據庫性能優化,你的思路是什么樣的?
或者具體一點,如果在面試的時候遇到這個問題:你會從哪些維度來優化數據庫,你會怎么回答?
我們在第一節課開始的時候講了,這四節課的目標是為了讓大家建立數據庫的知識體系,和正確的調優的思路
我們說到性能調優,大部分時候想要實現的目標是讓我們的查詢更快.一個查詢的動作又是由很多個環節組成的,每個環節都會消耗時間,我們在第一節課講 SQL 語句的執行流程的時候已經分析過了
我們要減少查詢所消耗的時間,就要從每一個環節入手.

連接——配置優化

第一個環節是客戶端連接到服務端,連接這一塊有可能會出現什么樣的性能問題?有可能是服務端連接數不夠導致應用程序獲取不到連接.比如報了一個 Mysql:error1040:Toomanyconnections 的錯誤
我們可以從兩個方面來解決連接數不夠的問題:

  • 從服務端來說,我們可以增加服務端的可用連接數,如果有多個應用或者很多請求同時訪問數據庫,連接數不夠的時候,我們可以:

*修改配置參數增加可用連接數,修改 max_connections 的大小:

show variables like 'max_connections'; -- 修改最大連接數,當有多個應用連接的時候

*或者,或者及時釋放不活動的連接.交互式和非交互式的客戶端的默認超時時間都是28800秒,8小時,我們可以把這個值調小

show global variables like 'wait_timeout'; --及時釋放不活動的連接,注意不要釋放連接池還在使用的連接
  • 從客戶端來說,可以減少從服務端獲取的連接數,如果我們想要不是每一次執行 SQL 都創建一個新的連接,應該怎么做?
    這個時候我們可以引入連接池,實現連接的重用
    我們可以在哪些層面使用連接池?ORM 層面(MyBatis 自帶了一個連接池);或者使用專用的連接池工具(阿里的 Druid,SpringBoot2.x 版本默認的連接池 Hikari,老牌的 DBCP 和 C3P0)
    當客戶端改成從連接池獲取連接之后,連接池的大小應該怎么設置呢?大家可能會有一個誤解,覺得連接池的最大連接數越大越好,這樣在高并發的情況下客戶端可以獲取的連接數更多,不需要排隊
    實際情況并不是這樣.連接池并不是越大越好,只要維護一定數量大小的連接池,其他的客戶端排隊等待獲取連接就可以了.有的時候連接池越大,效率反而越低
    Druid 的默認最大連接池大小是 8.Hikari 的默認最大連接池大小是 10
    為什么默認值都是這么小呢?
    在 Hikari 的 github 文檔中,給出了一個 PostgreSQL 數據庫建議的設置連接池大小的公式:
    https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing
    它的建議是機器核數乘以2加1.也就是說,4核的機器,連接池維護9個連接就夠了.這個公式從一定程度上來說對其他數據庫也是適用的.這里面還有一個減少連接池大小實現提升并發度和吞吐量的案例

為什么有的情況下,減少連接數反而會提升吞吐量呢?為什么建議設置的連接池大小要跟 CPU 的核數相關呢?
每一個連接,服務端都需要創建一個線程去處理它.連接數越多,服務端創建的線程數就會越多
問題:CPU 是怎么同時執行遠遠超過它的核數大小的任務的?時間片.上下文切換
而 CPU 的核數是有限的,頻繁的上下文切換會造成比較大的性能開銷
我們這里說到了從數據庫配置的層面去優化數據庫.不管是數據庫本身的配置,還是安裝這個數據庫服務的操作系統的配置,對于配置進行優化,最終的目標都是為了更好地發揮硬件本身的性能,包括 CPU,內存,磁盤,網絡
在不同的硬件環境下,操作系統和 MySQL 的參數的配置是不同的,沒有標準的配置
在我們這幾天的課程里面也接觸了很多的 MySQL 和 InnoDB 的配置參數,包括各種開關和數值的配置,大多數參數都提供了一個默認值,比如默認的 buffer_pool_size,默認的頁大小,InnoDB 并發線程數等等
這些默認配置可以滿足大部分情況的需求,除非有特殊的需求,在清楚參數的含義的情況下再去修改它.修改配置的工作一般由專業的 DBA 完成

至于硬件本身的選擇,比如使用固態硬盤,搭建磁盤陣列,選擇特定的 CPU 型號這些,更不是我們開發人員關注的重點,這個我們就不做過多的介紹了
如果想要了解一些特定的參數的含義,官網有一份系統的參數列表可以參考:
https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html
除了合理設置服務端的連接數和客戶端的連接池大小之外,我們還有哪些減少客戶端跟數據庫服務端的連接數的方案呢?
我們可以引入緩存

緩存——架構優化

緩存

在應用系統的并發數非常大的情況下,如果沒有緩存,會造成兩個問題:一方面是會給數據庫帶來很大的壓力.另一方面,從應用的層面來說,操作數據的速度也會受到影響
我們可以用第三方的緩存服務來解決這個問題,例如 Redis

運行獨立的緩存服務,屬于架構層面的優化
為了減少單臺數據庫服務器的讀寫壓力,在架構層面我們還可以做其他哪些優化措施?

主從復制

如果單臺數據庫服務滿足不了訪問需求,那我們可以做數據庫的集群方案
集群的話必然會面臨一個問題,就是不同的節點之間數據一致性的問題.如果同時讀寫多臺數據庫節點,怎么讓所有的節點數據保持一致?
這個時候我們需要用到復制技術(replication),被復制的節點稱為 master,復制的節點稱為 slave.slave 本身也可以作為其他節點的數據來源,這個叫做級聯復制

主從復制是怎么實現的呢?更新語句會記錄 binlog,它是一種邏輯日志
有了這個 binlog,從服務器會獲取主服務器的 binlog 文件,然后解析里面的 SQL 語句,在從服務器上面執行一遍,保持主從的數據一致
這里面涉及到三個線程,連接到 master 獲取 binlog,并且解析 binlog 寫入中繼日志,這個線程叫做 I/O 線程
Master 節點上有一個 logdump 線程,是用來發送 binlog 給 slave 的
從庫的 SQL 線程,是用來讀取 relaylog,把數據寫入到數據庫的

做了主從復制的方案之后,我們只把數據寫入 master 節點,而讀的請求可以分擔到 slave 節點.我們把這種方案叫做讀寫分離

讀寫分離可以一定程度低減輕數據庫服務器的訪問壓力,但是需要特別注意主從數據一致性的問題.如果我們在 master 寫入了,馬上到 slave 查詢,而這個時候 slave 的數據還沒有同步過來,怎么辦?
所以,基于主從復制的原理,我們需要弄明白,主從復制到底慢在哪里

單線程

在早期的 MySQL 中,slave 的 SQL 線程是單線程.master 可以支持 SQL 語句的并行執行,配置了多少的最大連接數就是最多同時多少個 SQL 并行執行
而 slave 的 SQL 卻只能單線程排隊執行,在主庫并發量很大的情況下,同步數據肯定會出現延遲
為什么從庫上的 SQLThread 不能并行執行呢?舉個例子,主庫執行了多條 SQL 語句,首先用戶發表了一條評論,然后修改了內容,最后把這條評論刪除了.這三條語句在從庫上的執行順序肯定是不能顛倒的

insert into user_comments (10000009,'nice');
update user_comments set content ='very good' where id =10000009;
delete from user_comments where id =10000009;

怎么解決這個問題呢?怎么減少主從復制的延遲?

異步與全同步

首先我們需要知道,在主從復制的過程中,MySQL 默認是異步復制的.也就是說,對于主節點來說,寫入 binlog,事務結束,就返回給客戶端了.對于 slave 來說,接收到 binlog,就完事兒了,master 不關心 slave 的數據有沒有寫入成功

如果要減少延遲,是不是可以等待全部從庫的事務執行完畢,才返回給客戶端呢?這樣的方式叫做全同步復制.從庫寫完數據,主庫才返會給客戶端

這種方式雖然可以保證在讀之前,數據已經同步成功了,但是帶來的副作用大家應該能想到,事務執行的時間會變長,它會導致 master 節點性能下降
有沒有更好的辦法呢?既減少 slave 寫入的延遲,又不會明顯增加 master 返回給客戶端的時間

半同步復制

介于異步復制和全同步復制之間,還有一種半同步復制的方式
半同步復制是什么樣的呢?
主庫在執行完客戶端提交的事務后不是立刻返回給客戶端,而是等待至少一個從庫接收到 binlog 并寫到 relaylog 中才返回給客戶端.master 不會等待很長的時間,但是返回給客戶端的時候,數據就即將寫入成功了,因為它只剩最后一步了:就是讀取 relaylog,寫入從庫

如果我們要在數據庫里面用半同步復制,必須安裝一個插件,這個是谷歌的一位工程師貢獻的.這個插件在 mysql 的插件目錄下已經有提供:

cd /usr/lib64/mysql/plugin/

主庫和從庫是不同的插件,安裝之后需要啟用:

-- 主庫執行
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
set global rpl_semi_sync_master_enabled=1;
show variables like '%semi_sync%';

-- 從庫執行
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
set global rpl_semi_sync_slave_enabled=1;
show global variables like '%semi%';

相對于異步復制,半同步復制提高了數據的安全性,同時它也造成了一定程度的延遲,它需要等待一個 slave 寫入中繼日志,這里多了一個網絡交互的過程,所以,半同步復制最好在低延時的網絡中使用
這個是從主庫和從庫連接的角度,來保證 slave 數據的寫入
另一個思路,如果要減少主從同步的延遲,減少 SQL 執行造成的等待的時間,那有沒有辦法在從庫上,讓多個 SQL 語句可以并行執行,而不是排隊執行呢?

多庫并行復制

怎么實現并行復制呢?設想一下,如果 3 條語句是在三個數據庫執行,操作各自的數據庫,是不是肯定不會產生并發的問題呢?執行的順序也沒有要求.當然是,所以如果是操作三個數據庫,這三個數據庫的從庫的 SQL 線程可以并發執行.這是 MySQL5.6 版本里面支持的多庫并行復制

但是在大部分的情況下,我們都是單庫多表的情況,在一個數據庫里面怎么實現并行復制呢?或者說,我們知道,數據庫本身就是支持多個事務同時操作的;為什么這些事務在主庫上面可以并行執行,卻不會出現問題呢?
因為他們本身就是互相不干擾的,比如這些事務是操作不同的表,或者操作不同的行,不存在資源的競爭和數據的干擾.那在主庫上并行執行的事務,在從庫上肯定也是可以并行執行,是不是?比如在 master 上有三個事務同時分別操作三張表,這三個事務是不是在 slave 上面也可以并行執行呢

異步復制之 GTID 復制

https://dev.mysql.com/doc/refman/5.7/en/replication-gtids.html
所以,我們可以把那些在主庫上并行執行的事務,分為一個組,并且給他們編號,這一個組的事務在從庫上面也可以并行執行.這個編號,我們把它叫做 GTID(GlobalTransactionIdentifiers),這種主從復制的方式,我們把它叫做基于 GTID 的復制

如果我們要使用 GTID 復制,我們可以通過修改配置參數打開它,默認是關閉的:

show global variables like 'gtid_mode';

無論是優化 master 和 slave 的連接方式,還是讓從庫可以并行執行 SQL,都是從數據庫的層面去解決主從復制延遲的問題
除了數據庫本身的層面之外,在應用層面,我們也有一些減少主從同步延遲的方法
我們在做了主從復制之后,如果單個 master 節點或者單張表存儲的數據過大的時候,比如一張表有上億的數據,單表的查詢性能還是會下降,我們要進一步對單臺數據庫節點的數據分型拆分,這個就是分庫分表

分庫分表

垂直分庫,減少并發壓力.水平分表,解決存儲瓶頸
垂直分庫的做法,把一個數據庫按照業務拆分成不同的數據庫:

水平分庫分表的做法,把單張表的數據按照一定的規則分布到多個數據庫

通過主從或者分庫分表可以減少單個數據庫節點的訪問壓力和存儲壓力,達到提升數據庫性能的目的,但是如果 master 節點掛了,怎么辦?
所以,高可用(HighAvailable)也是高性能的基礎

高可用方案

https://dev.mysql.com/doc/mysql-ha-scalability/en/ha-overview.html

主從復制

傳統的 HAProxy+keepalived 的方案,基于主從復制

NDB Cluster

https://dev.mysql.com/doc/mysql-cluster-excerpt/5.7/en/mysql-cluster-overview.html
基于 NDB 集群存儲引擎的 MySQLCluster

Galera

https://galeracluster.com/
一種多主同步復制的集群方案

MHA/MMM

https://tech.meituan.com/2017/06/29/database-availability-architecture.html
MMM(Master-MasterreplicationmanagerforMySQL),一種多主的高可用架構,是一個日本人開發的,像美團這樣的公司早期也有大量使用 MMM
MHA(MySQLMasterHighAvailable)
MMM 和 MHA 都是對外提供一個虛擬 IP,并且監控主節點和從節點,當主節點發生故障的時候,需要把一個從節點提升為主節點,并且把從節點里面比主節點缺少的數據補上,把 VIP 指向新的主節點

MGR

https://dev.mysql.com/doc/refman/5.7/en/group-replication.html
https://dev.mysql.com/doc/refman/5.7/en/mysql-cluster.html

MySQL5.7.17 版本推出的 InnoDBCluster,也叫 MySQLGroupReplicatioin(MGR),這個套件里面包括了 mysqlshell 和 mysql-route

總結一下:
高可用 HA 方案需要解決的問題都是當一個 master 節點宕機的時候,如何提升一個數據最新的 slave 成為 master.如果同時運行多個 master,又必須要解決 master 之間數據復制,以及對于客戶端來說連接路由的問題
不同的方案,實施難度不一樣,運維管理的成本也不一樣
以上是架構層面的優化,可以用緩存,主從,分庫分表

第三個環節:解析器,詞法和語法分析,主要保證語句的正確性,語句不出錯就沒問題.由 Sever 自己處理,跳過
第四步:優化器

優化器--SQL 語句分析與優化

優化器就是對我們的 SQL 語句進行分析,生成執行計劃
問題:在我們做項目的時候,有時會收到 DBA 的郵件,里面列出了我們項目上幾個耗時比較長的查詢語句,讓我們去優化,這些語句是從哪里來的呢?
我們的服務層每天執行了這么多 SQL 語句,它怎么知道哪些 SQL 語句比較慢呢?
第一步,我們要把 SQL 執行情況記錄下來

慢查詢日志 slow query log

https://dev.mysql.com/doc/refman/5.7/en/slow-query-log.html

打開慢日志開關

因為開啟慢查詢日志是有代價的(跟 binlog,optimizer-trace 一樣),所以它默認是關閉的:

show variables like 'slow_query%';

除了這個開關,還有一個參數,控制執行超過多長時間的 SQL 才記錄到慢日志,默認是 10 秒

show variables like '%slow_query%';

可以直接動態修改參數(重啟后失效)

set @@global.slow_query_log=1; -- 1 開啟,0 關閉,重啟后失效
set @@global.long_query_time=3; -- mysql 默認的慢查詢時間是 10 秒,另開一個窗口后才會查到最新值
show variables like '%long_query%';
show variables like '%slow_query%';

或者修改配置文件 my.cnf
以下配置定義了慢查詢日志的開關,慢查詢的時間,日志文件的存放路徑

slow_query_log = ON
long_query_time=2
slow_query_log_file =/var/lib/mysql/localhost-slow.log

模擬慢查詢:

select sleep(10);

查詢 user_innodb 表的 500 萬數據(檢查是不是沒有索引)

SELECT * FROM `user_innodb` where phone = '136';

慢日志分析

日志內容

show global status like 'slow_queries'; -- 查看有多少慢查詢
show variables like '%slow_query%'; -- 獲取慢日志目錄

cat /var/lib/mysql/ localhost-slow.log

有了慢查詢日志,怎么去分析統計呢?比如 SQL 語句的出現的慢查詢次數最多,平均每次執行了多久?

mysqldumpslow

https://dev.mysql.com/doc/refman/5.7/en/mysqldumpslow.html
MySQL 提供了 mysqldumpslow 的工具,在 MySQL 的 bin 目錄下

mysqldumpslow --help

例如:查詢用時最多的 20 條慢 SQL:

mysqldumpslow -s t -t 20 -g 'select' /var/lib/mysql/localhost-slow.log
  • Count 代表這個 SQL 執行了多少次
  • Time 代表執行的時間,括號里面是累計時間
  • Lock 表示鎖定的時間,括號是累計
  • Rows 表示返回的記錄數,括號是累計

除了慢查詢日志之外,還有一個 SHOWPROFILE 工具可以使用

SHOW PROFILE

https://dev.mysql.com/doc/refman/5.7/en/show-profile.html

SHOWPROFILE 是谷歌高級架構師 JeremyCole 貢獻給 MySQL 社區的,可以查看 SQL 語句執行的時候使用的資源,比如 CPU,IO 的消耗情況
在 SQL 中輸入 helpprofile 可以得到詳細的幫助信息

查看是否開啟

select @@profiling;
set @@profiling=1;

查看 profile 統計

(命令最后帶一個 s)

show profiles;

查看最后一個 SQL 的執行詳細信息,從中找出耗時較多的環節(沒有 s)

show profile;

6.2E-5,小數點左移 5 位,代表 0.000062 秒

也可以根據 ID 查看執行詳細信息,在后面帶上 forquery+ID

show profile for query 1;

除了慢日志和 showprofile,如果要分析出當前數據庫中執行的慢的 SQL,還可以通過查看運行線程狀態和服務器運行信息,存儲引擎信息來分析

其他系統命令

show processlist 運行線程

https://dev.mysql.com/doc/refman/5.7/en/show-processlist.html

show processlist;

這是很重要的一個命令,用于顯示用戶運行線程.可以根據 id 號 kill 線程,也可以查表,效果一樣:

select * from information_schema.processlist;
含義
Id 線程的唯一標志,可以根據它 kill 線程
User 啟動這個線程的用戶,普通用戶只能看到自己的線程
Host 哪個 IP 端口發起的連接
db 操作的數據庫
Command 線程的命令 https://dev.mysql.com/doc/refman/5.7/en/thread-commands.html
Time 操作持續時間,單位秒
State 線程狀態,比如查詢可能有 copyingtotmptable,Sortingresult,Sendingdatahttps://dev.mysql.com/doc/refman/5.7/en/general-thread-states.html
Info SQL 語句的前 100 個字符,如果要查看完整的 SQL 語句,用 SHOWFULLPROCESSLIST

show status 服務器運行狀態

https://dev.mysql.com/doc/refman/5.7/en/show-status.html

SHOWSTATUS 用于查看 MySQL 服務器運行狀態(重啟后會清空),有 session 和 global 兩種作用域,格式:參數-值
可以用 like 帶通配符過濾

SHOW GLOBAL STATUS LIKE 'com_select'; -- 查看 select 次數

show engine 存儲引擎運行信息

https://dev.mysql.com/doc/refman/5.7/en/show-engine.html

showengine 用來顯示存儲引擎的當前運行信息,包括事務持有的表鎖,行鎖信息;事務的鎖等待情況;線程信號量等待;文件 IO 請求;bufferpool 統計信息
例如:

show engine innodb status;

如果需要將監控信息輸出到錯誤信息 errorlog 中(15 秒鐘一次),可以開啟輸出

show variables like 'innodb_status_output%';

-- 開啟輸出:
SET GLOBAL innodb_status_output=ON;
SET GLOBAL innodb_status_output_locks=ON;

我們現在已經知道了這么多分析服務器狀態,存儲引擎狀態,線程運行信息的命令,如果讓你去寫一個數據庫監控系統,你會怎么做?
其實很多開源的慢查詢日志監控工具,他們的原理其實也都是讀取的系統的變量和狀態
現在我們已經知道哪些 SQL 慢了,為什么慢呢?慢在哪里?
MySQL 提供了一個執行計劃的工具(在架構中我們有講到,優化器最終生成的就是一個執行計劃),其他數據庫,例如 Oracle 也有類似的功能
通過 EXPLAIN 我們可以模擬優化器執行 SQL 查詢語句的過程,來知道 MySQL 是怎么處理一條 SQL 語句的.通過這種方式我們可以分析語句或者表的性能瓶頸

explain 可以分析 update,delete,insert 么?
MySQL5.6.3 以前只能分析 SELECT;MySQL5.6.3 以后就可以分析 update,delete,insert 了

EXPLAIN 執行計劃

官方鏈接:https://dev.mysql.com/doc/refman/5.7/en/explain-output.html

我們先創建三張表.一張課程表,一張老師表,一張老師聯系方式表(沒有任何索引)

DROP TABLE IF EXISTS course;
CREATE TABLE `course` (
    `cid` int(3)DEFAULT NULL,
    `cname` varchar(20)DEFAULT NULL,
    `tid` int(3)DEFAULT NULL 23
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS teacher;
CREATE TABLE `teacher` (
    `tid` int(3)DEFAULT NULL,
    `tname` varchar(20)DEFAULT NULL,
    `tcid` int(3)DEFAULT NULL
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DROP TABLE IF EXISTS teacher_contact;
CREATE TABLE `teacher_contact` (
    `tcid` int(3)DEFAULT NULL,
    `phone` varchar(200)DEFAULT NULL
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `course` VALUES ('1', 'mysql', '1');
INSERT INTO `course` VALUES ('2', 'jvm', '1');
INSERT INTO `course` VALUES ('3', 'juc', '2');
INSERT INTO `course` VALUES ('4', 'spring', '3');
INSERT INTO `teacher` VALUES ('1', 'qingshan', '1');
INSERT INTO `teacher` VALUES ('2', 'jack', '2');
INSERT INTO `teacher` VALUES ('3', 'mic', '3');
INSERT INTO `teacher_contact` VALUES ('1', '13688888888');
INSERT INTO `teacher_contact` VALUES ('2', '18166669999');
INSERT INTO `teacher_contact` VALUES ('3', '17722225555');

explain 的結果有很多的字段,我們詳細地分析一下.先確認一下環境:

select version();
show variables like '%engine%';

id

id 是查詢序列編號

id 值不同

id 值不同的時候,先查詢 id 值大的(先大后小)

-- 查詢 mysql 課程的老師手機號
EXPLAIN SELECT tc.phone
FROM teacher_contact tc
WHERE tcid = (
    SELECT tcid FROM teacher t
    WHERE t.tid = (
        SELECT c.tid FROM course c
        WHERE c.cname = 'mysql'
    )
);

查詢順序:coursec——teachert——teacher_contacttc

先查課程表,再查老師表,最后查老師聯系方式表.子查詢只能以這種方式進行,只有拿到內層的結果之后才能進行外層的查詢

id 值相同

-- 查詢課程 ID 為 2,或者聯系表 ID 為 3 的老師
EXPLAIN SELECT t.tname,c.cname,tc.phone
FROM teacher t, course c, teacher_contact tc
WHERE t.tid = c.tid AND t.tcid = tc.tcid AND (c.cid = 2 OR tc.tcid = 3);

id 值相同時,表的查詢順序是從上往下順序執行.例如這次查詢的 id 都是 1,查詢的順序是 teachert(3 條)——coursec(4 條)——teacher_contacttc(3 條)
teacher 表插入 3 條數據后:

INSERT INTO `teacher` VALUES (4, 'james', 4);
INSERT INTO `teacher` VALUES (5, 'tom', 5);
INSERT INTO `teacher` VALUES (6, 'seven', 6);
COMMIT;

-- (備份)恢復語句
DELETE FROM teacher where tid in (4,5,6);
COMMIT;

id 也都是 1,但是從上往下查詢順序變成了:teacher_contacttc(3 條)——teachert(6 條)——coursec(4 條)

為什么數據量不同的時候順序會發生變化呢?這個是由笛卡爾積決定的
舉例:假如有 a,b,c 三張表,分別有 2,3,4 條數據,如果做三張表的聯合查詢,當查詢順序是 a→b→c 的時候,它的笛卡爾積是:2*3*4=24.如果查詢順序是 c→b→a,它的笛卡爾積是 4*3*2=24
因為 MySQL 要把查詢的結果,包括中間結果和最終結果都保存到內存,所以 MySQL 會優先選擇中間結果數據量比較小的順序進行查詢.所以最終聯表查詢的順序是 a→b→c.這個就是為什么 teacher 表插入數據以后查詢順序會發生變化
(小標驅動大表的思想)

既有相同也有不同

如果 ID 有相同也有不同,就是 ID 不同的先大后小,ID 相同的從上往下

select type 查詢類型

這里并沒有列舉全部(其它:DEPENDENTUNION,DEPENDENTSUBQUERY,MATERIALIZED,UNCACHEABLESUBQUERY,UNCACHEABLEUNION)
下面列舉了一些常見的查詢類型:

SIMPLE

簡單查詢,不包含子查詢,不包含關聯查詢 union

EXPLAIN SELECT * FROM teacher;

再看一個包含子查詢的案例:

-- 查詢 mysql 課程的老師手機號
EXPLAIN SELECT tc.phone FROM teacher_contact tc
WHERE tcid = (
    SELECT tcid FROM teacher t
    WHERE t.tid = (
        SELECT c.tid FROM course c WHERE c.cname = 'mysql'
    )
);

PRIMARY

子查詢 SQL 語句中的主查詢,也就是最外面的那層查詢

SUBQUERY

子查詢中所有的內層查詢都是 SUBQUERY 類型的

DERIVED

衍生查詢,表示在得到最終查詢結果之前會用到臨時表.例如:

-- 查詢 ID 為 1 或 2 的老師教授的課程
EXPLAIN SELECT cr.cname FROM (
    SELECT * FROM course WHERE tid = 1
    UNION SELECT * FROM course WHERE tid = 2
)cr;

對于關聯查詢,先執行右邊的 table(UNION),再執行左邊的 table,類型是 DERIVED

UNION

用到了 UNION 查詢.同上例

UNION RESULT

主要是顯示哪些表之間存在 UNION 查詢.<union2,3>代表 id=2 和 id=3 的查詢存在 UNION.同上例

type 連接類型

https://dev.mysql.com/doc/refman/5.7/en/explain-output.html#explain-join-types

所有的連接類型中,上面的最好,越往下越差
在常用的鏈接類型中:system>const>eq_ref>ref>range>index>all
這里并沒有列舉全部(其他:fulltext,ref_or_null,index_merger,unique_subquery,index_subquery)
以上訪問類型除了 all,都能用到索引

const

主鍵索引或者唯一索引,只能查到一條數據的 SQL

DROP TABLE IF EXISTS single_data;
CREATE TABLE single_data(
id int(3)PRIMARY KEY,
content varchar(20)
);

insert into single_data values(1,'a');
EXPLAIN SELECT * FROM single_data a where id = 1;

system

system 是 const 的一種特例,只有一行滿足條件.例如:只有一條數據的系統表

EXPLAIN SELECT * FROM mysql.proxies_priv;

eq_ref

通常出現在多表的 join 查詢,表示對于前表的每一個結果,都只能匹配到后表的一行結果.一般是唯一性索引的查詢(UNIQUE 或 PRIMARYKEY)
eq_ref 是除 const 之外最好的訪問類型
先刪除 teacher 表中多余的數據,teacher_contact 有 3 條數據,teacher 表有 3 條數據

DELETE FROM teacher where tid in (4,5,6);
commit;

-- 備份
INSERT INTO `teacher` VALUES (4, 'james', 4);
INSERT INTO `teacher` VALUES (5, 'tom', 5);
INSERT INTO `teacher` VALUES (6, 'seven', 6);

commit;

為 teacher_contact 表的 tcid(第一個字段)創建主鍵索引

-- ALTER TABLE teacher_contact DROP PRIMARY KEY;
ALTER TABLE teacher_contact ADD PRIMARY KEY(tcid);

為 teacher 表的 tcid(第三個字段)創建普通索引

-- ALTER TABLE teacher DROP INDEX idx_tcid;
ALTER TABLE teacher ADD INDEX idx_tcid (tcid);

執行以下 SQL 語句:

select t.tcid from teacher t,teacher_contact tc where t.tcid = tc.tcid;

此時的執行計劃(teacher_contact 表是 eq_ref):

小結:
以上三種 system,const,eq_ref,都是可遇而不可求的,基本上很難優化到這個狀態

ref

查詢用到了非唯一性索引,或者關聯操作只使用了索引的最左前綴
例如:使用 tcid 上的普通索引查詢:

explain SELECT * FROM teacher where tcid = 3;

range

索引范圍掃描
如果 where 后面是 betweenand 或<或>或>=或<=或 in 這些,type 類型就為 range
不走索引一定是全表掃描(ALL),所以先加上普通索引

-- ALTER TABLE teacher DROP INDEX idx_tid;
ALTER TABLE teacher ADD INDEX idx_tid (tid);

執行范圍查詢(字段上有普通索引):

EXPLAIN SELECT * FROM teacher t WHERE t.tid <3;
-- 或
EXPLAIN SELECT * FROM teacher t WHERE tid BETWEEN 1 AND 2;

IN 查詢也是 range(字段有主鍵索引)

EXPLAIN SELECT * FROM teacher_contact t WHERE tcid in (1,2,3);

index

FullIndexScan,查詢全部索引中的數據(比不走索引要快)

EXPLAIN SELECT tid FROM teacher;

all

FullTableScan,如果沒有索引或者沒有用到索引,type 就是 ALL.代表全表掃描

NULL

不用訪問表或者索引就能得到結果,例如:

EXPLAIN select 1 from dual where 1=1;

小結:
一般來說,需要保證查詢至少達到 range 級別,最好能達到 ref
ALL(全表掃描)和 index(查詢全部索引)都是需要優化的

possible_key,key

可能用到的索引和實際用到的索引.如果是 NULL 就代表沒有用到索引
possible_key 可以有一個或者多個,可能用到索引不代表一定用到索引
反過來,possible_key 為空,key 可能有值嗎?
表上創建聯合索引:

ALTER TABLE user_innodb DROP INDEX comidx_name_phone;
ALTER TABLE user_innodb add INDEX comidx_name_phone (name,phone);

執行計劃(改成 selectname 也能用到索引):

explain select phone from user_innodb where phone='126';

結論:是有可能的(這里是覆蓋索引的情況)
如果通過分析發現沒有用到索引,就要檢查 SQL 或者創建索引

key_len

索引的長度(使用的字節數).跟索引字段的類型,長度有關

rows

MySQL 認為掃描多少行才能返回請求的數據,是一個預估值.一般來說行數越少越好

filtered

這個字段表示存儲引擎返回的數據在 server 層過濾后,剩下多少滿足查詢的記錄數量的比例,它是一個百分比

ref

使用哪個列或者常數和索引一起從表中篩選數據

Extra

執行計劃給出的額外的信息說明

using index

用到了覆蓋索引,不需要回表

EXPLAIN SELECT tid FROM teacher;

using where

使用了 where 過濾,表示存儲引擎返回的記錄并不是所有的都滿足查詢條件,需要在 server 層進行過濾(跟是否使用索引沒有關系)

EXPLAIN select * from user_innodb where phone ='13866667777';

Using index condition(索引條件下推)

索引下推,在第二節課中已經講解過了
https://dev.mysql.com/doc/refman/5.7/en/index-condition-pushdown-optimization.html

using filesort

不能使用索引來排序,用到了額外的排序(跟磁盤或文件沒有關系).需要優化(復合索引的前提)

ALTER TABLE user_innodb DROP INDEX comidx_name_phone;
ALTER TABLE user_innodb add INDEX comidx_name_phone (name,phone);

EXPLAIN select * from user_innodb where name ='青山' order by id;

(orderbyid 引起)

using temporary

用到了臨時表.例如(以下不是全部的情況):

  • distinct 非索引列
EXPLAIN select DISTINCT(tid)from teacher t;
  • groupby 非索引列
EXPLAIN select tname from teacher group by tname;
  • 使用 join 的時候,group 任意列
EXPLAIN select t.tid from teacher t join course c on t.tid = c.tid group by t.tid;

需要優化,例如創建復合索引

總結一下:
模擬優化器執行 SQL 查詢語句的過程,來知道 MySQL 是怎么處理一條 SQL 語句的.通過這種方式我們可以分析語句或者表的性能瓶頸
分析出問題之后,就是對 SQL 語句的具體優化
比如怎么用到索引,怎么減少鎖的阻塞等待,在前面兩次課已經講過

SQL 與索引優化

當我們的 SQL 語句比較復雜,有多個關聯和子查詢的時候,就要分析 SQL 語句有沒有改寫的方法
舉個簡單的例子,一模一樣的數據:

-- 大偏移量的 limit
select * from user_innodb limit 900000,10;
-- 改成先過濾 ID,再 limit
SELECT * FROM user_innodb WHERE id >= 900000 LIMIT 10;

對于具體的 SQL 語句的優化,MySQL 官網也提供了很多建議,這個是我們在分析具體的 SQL 語句的時候需要注意的,也是大家在以后的工作里面要去慢慢地積累的(這里我們就不一一地分析了)

https://dev.mysql.com/doc/refman/5.7/en/optimization.html

存儲引擎

存儲引擎的選擇

為不同的業務表選擇不同的存儲引擎,例如:查詢插入操作多的業務表,用 MyISAM.臨時數據用 Memeroy.常規的并發大更新多的表用 InnoDB.

分區或者分表

分區不推薦
交易歷史表:在年底為下一年度建立12個分區,每個月一個分區
渠道交易表:分成當日表;當月表;歷史表,歷史表再做分區

字段定義

原則:使用可以正確存儲數據的最小數據類型
為每一列選擇合適的字段類型:

整數類型

INT 有 8 種類型,不同的類型的最大存儲范圍是不一樣的
性別?用 TINYINT,因為 ENUM 也是整型存儲

字符類型

變長情況下,varchar 更節省空間,但是對于 varchar 字段,需要一個字節來記錄長度
固定長度的用 char,不要用 varchar

非空

非空字段盡量定義成 NOTNULL,提供默認值,或者使用特殊值,空串代替 null
NULL 類型的存儲,優化,使用都會存在問題

不要用外鍵,觸發器,視圖

降低了可讀性
影響數據庫性能,應該把把計算的事情交給程序,數據庫專心做存儲
數據的完整性應該在程序中檢查

大文件存儲

不要用數據庫存儲圖片(比如 base64 編碼)或者大文件
把文件放在 NAS 上,數據庫只需要存儲 URI(相對路徑),在應用中配置 NAS 服務器地址

表拆分

將不常用的字段拆分出去,避免列數過多和數據量過大
比如在業務系統中,要記錄所有接收和發送的消息,這個消息是 XML 格式的,用 blob 或者 text 存儲,用來追蹤和判斷重復,可以建立一張表專門用來存儲報文

總結:優化體系

除了對于代碼,SQL 語句,表定義,架構,配置優化之外,業務層面的優化也不能忽視.舉幾個例子:

  • 在某一年的雙十一,為什么會做一個充值到余額寶和余額有獎金的活動(充300送50)?

因為使用余額或者余額寶付款是記錄本地或者內部數據庫,而使用銀行卡付款,需要調用接口,操作內部數據庫肯定更快

  • 在去年的雙十一,為什么在凌晨禁止查詢今天之外的賬單?

這是一種降級措施,用來保證當前最核心的業務

  • 最近幾年的雙十一,為什么提前一個多星期就已經有雙十一當天的價格了?

預售分流

在應用層面同樣有很多其他的方案來優化,達到盡量減輕數據庫的壓力的目的,比如限流,或者引入 MQ 削峰,等等等等
為什么同樣用 MySQL,有的公司可以扛住百萬千萬級別的并發,而有的公司幾百個并發都扛不住,關鍵在于怎么用.所以,用數據庫慢,不代表數據庫本身慢,有的時候還要往上層去優化
當然,如果關系型數據庫解決不了的問題,我們可能需要用到搜索引擎或者大數據的方案了,并不是所有的數據都要放到關系型數據庫存儲

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容