常見性能優化實踐總結

常見性能優化實踐總結

一:代碼

這一點最容易引起技術人員的忽視。很多技術人員拿到一個性能優化的需求以後,言必稱緩存、異步、JVM等。有一些性能問題,完全是由於代碼寫的不合理,通過直接修改一下代碼就能解決問題的,比如for循環次數過多、作了很多無謂的條件判斷、相同邏輯重複多次等。

二:數據庫

數據庫的調優,總的來說分爲以下三部分:

SQL調優

這是最常用、每一個技術人員都應該掌握基本的SQL調優手段(包括方法、工具、輔助系統等)。這裏以MySQL爲例,最常見的方式是,由自帶的慢查詢日誌或者開源的慢查詢系統定位到具體的出問題的SQL,然後使用explain、profile等工具來逐步調優,最後經過測試達到效果後上線。這方面的細節,可以參考MySQL索引原理及慢查詢優化。

架構調優

這一類調優包括讀寫分離、多從庫負載均衡、水平和垂直分庫分表等方面,一般需要的改動較大,但是頻率沒有SQL調優高,而且一般需要DBA來配合參與。那麼什麼時候需要做這些事情?

我們可以通過內部監控報警系統(比如Zabbix),定期跟蹤一些指標數據是否達到瓶頸,一旦達到瓶頸或者警戒值,就需要考慮這些事情。通常,DBA也會定期監控這些指標值。

連接池調優

我們的應用爲了實現數據庫連接的高效獲取、對數據庫連接的限流等目的,通常會採用連接池類的方案,即每一個應用節點都管理了一個到各個數據庫的連接池。

隨着業務訪問量或者數據量的增長,原有的連接池參數可能不能很好地滿足需求,這個時候就需要結合當前使用連接池的原理、具體的連接池監控數據和當前的業務量作一個綜合的判斷,通過反覆的幾次調試得到最終的調優參數。

三:緩存

分類

本地緩存(HashMap/ConcurrentHashMap、Ehcache、Guava Cache等),緩存服務(Redis/Tair/Memcache等)。

使用場景

什麼情況適合用緩存?考慮以下兩種場景:

  • 短時間內相同數據重複查詢多次且數據更新不頻繁,這個時候可以選擇先從緩存查詢,查詢不到再從數據庫加載並回設到緩存的方式。此種場景較適合用單機緩存。
  • 高併發查詢熱點數據,後端數據庫不堪重負,可以用緩存來扛。

選型考慮

如果數據量小,並且不會頻繁地增長又清空(這會導致頻繁地垃圾回收),那麼可以選擇本地緩存。

具體的話,如果需要一些策略的支持(比如緩存滿的逐出策略),可以考慮Ehcache;如不需要,可以考慮HashMap;如需要考慮多線程併發的場景,可以考慮ConcurentHashMap。

其他情況,可以考慮緩存服務。目前從資源的投入度、可運維性、是否能動態擴容以及配套設施來考慮,我們優先考慮Tair。除非目前Tair還不能支持的場合(比如分佈式鎖、Hash類型的value),我們考慮用Redis。

設計關鍵點

什麼時候更新緩存?如何保障更新的可靠性和實時性?

更新緩存的策略,需要具體問題具體分析。這裏以門店POI的緩存數據爲例,來說明一下緩存服務型的緩存更新策略是怎樣的?目前約10萬個POI數據採用了Tair作爲緩存服務,具體更新的策略有兩個:

  • 接收門店變更的消息,準實時更新。
  • 給每一個POI緩存數據設置5分鐘的過期時間,過期後從DB加載再回設到DB。這個策略是對第一個策略的有力補充,解決了手動變更DB不發消息、接消息更新程序臨時出錯等問題導致的第一個策略失效的問題。通過這種雙保險機制,有效地保證了POI緩存數據的可靠性和實時性。

緩存是否會滿,緩存滿了怎麼辦?

對於一個緩存服務,理論上來說,隨着緩存數據的日益增多,在容量有限的情況下,緩存肯定有一天會滿的。如何應對?

  • ① 給緩存服務,選擇合適的緩存逐出算法,比如最常見的LRU。
  • ② 針對當前設置的容量,設置適當的警戒值,比如10G的緩存,當緩存數據達到8G的時候,就開始發出報警,提前排查問題或者擴容。
  • ③ 給一些沒有必要長期保存的key,儘量設置過期時間。

緩存是否允許丟失?丟失了怎麼辦?

根據業務場景判斷,是否允許丟失。如果不允許,就需要帶持久化功能的緩存服務來支持,比如Redis或者Tair。更細節的話,可以根據業務對丟失時間的容忍度,還可以選擇更具體的持久化策略,比如Redis的RDB或者AOF。

緩存被“擊穿”問題

對於一些設置了過期時間的key,如果這些key可能會在某些時間點被超高併發地訪問,是一種非常“熱點”的數據。這個時候,需要考慮另外一個問題:緩存被“擊穿”的問題。

概念:緩存在某個時間點過期的時候,恰好在這個時間點對這個Key有大量的併發請求過來,這些請求發現緩存過期一般都會從後端DB加載數據並回設到緩存,這個時候大併發的請求可能會瞬間把後端DB壓垮。

如何解決:業界比較常用的做法,是使用mutex。簡單地來說,就是在緩存失效的時候(判斷拿出來的值爲空),不是立即去load db,而是先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一個mutex key,當操作返回成功時,再進行load db的操作並回設緩存;否則,就重試整個get緩存的方法。

四:異步

使用場景

針對某些客戶端的請求,在服務端可能需要針對這些請求做一些附屬的事情,這些事情其實用戶並不關心或者用戶不需要立即拿到這些事情的處理結果,這種情況就比較適合用異步的方式處理這些事情。

作用

縮短接口響應時間,使用戶的請求快速返回,用戶體驗更好。
避免線程長時間處於運行狀態,這樣會引起服務線程池的可用線程長時間不夠用,進而引起線程池任務隊列長度增大,從而阻塞更多請求任務,使得更多請求得不到技術處理。
線程長時間處於運行狀態,可能還會引起系統Load、CPU使用率、機器整體性能下降等一系列問題,甚至引發雪崩。異步的思路可以在不增加機器數和CPU數的情況下,有效解決這個問題。

常見做法

一種做法,是額外開闢線程,這裏可以採用額外開闢一個線程或者使用線程池的做法,在IO線程(處理請求響應)之外的線程來處理相應的任務,在IO線程中讓response先返回。

如果異步線程處理的任務設計的數據量非常巨大,那麼可以引入阻塞隊列BlockingQueue作進一步的優化。具體做法是讓一批異步線程不斷地往阻塞隊列裏扔數據,然後額外起一個處理線程,循環批量從隊列裏拿預設大小的一批數據,來進行批處理(比如發一個批量的遠程服務請求),這樣進一步提高了性能。

另一種做法,是使用消息隊列(MQ)中間件服務,MQ天生就是異步的。一些額外的任務,可能不需要我這個系統來處理,但是需要其他系統來處理。這個時候可以先把它封裝成一個消息,扔到消息隊列裏面,通過消息中間件的可靠性保證把消息投遞到關心它的系統,然後讓這個系統來做相應的處理。

比如C端在完成一個提單動作以後,可能需要其它端做一系列的事情,但是這些事情的結果不會立刻對C端用戶產生影響,那麼就可以先把C端下單的請求響應先返回給用戶,返回之前往MQ中發一個消息即可。而且這些事情理應不是C端的負責範圍,所以這個時候用MQ的方式,來解決這個問題最合適。

五:JVM調優

什麼時候調?

通過監控系統(如沒有現成的系統,自己做一個簡單的上報監控的系統也很容易)上對一些機器關鍵指標(gc time、gc count、各個分代的內存大小變化、機器的Load值與CPU使用率、JVM的線程數等)的監控報警,也可以看gc log和jstat等命令的輸出,再結合線上JVM進程服務的一些關鍵接口的性能數據和請求體驗,基本上就能定位出當前的JVM是否有問題,以及是否需要調優。

怎麼調?

如果發現高峯期CPU使用率與Load值偏大,這個時候可以觀察一些JVM的thread count以及gc count(可能主要是young gc count),如果這兩個值都比以往偏大(也可以和一個歷史經驗值作對比),基本上可以定位是young gc頻率過高導致,這個時候可以通過適當增大young區大小或者佔比的方式來解決。

如果發現關鍵接口響應時間很慢,可以結合gc time以及gc log中的stop the world的時間,看一下整個應用的stop the world的時間是不是比較多。如果是,可能需要減少總的gc time,具體可以從減小gc的次數和減小單次gc的時間這兩個維度來考慮,一般來說,這兩個因素是一對互斥因素,我們需要根據實際的監控數據來調整相應的參數(比如新生代與老生代比值、eden與survivor比值、MTT值、觸發cms回收的old區比率閾值等)來達到一個最優值。

如果發生full gc或者old cms gc非常頻繁,通常這種情況會誘發STW的時間相應加長,從而也會導致接口響應時間變慢。這種情況,大概率是出現了“內存泄露”,Java裏的內存泄露指的是一些應該釋放的對象沒有被釋放掉(還有引用拉着它)。那麼這些對象是如何產生的呢?爲啥不會釋放呢?對應的代碼是不是出問題了?問題的關鍵是搞明白這個,找到相應的代碼,然後對症下藥。

所以問題的關鍵是轉化成尋找這些對象。怎麼找?綜合使用jmap和MAT,基本就能定位到具體的代碼。

六:多線程與分佈式

使用場景

離線任務、異步任務、大數據任務、耗時較長任務的運行**,適當地利用,可達到加速的效果。

注意:線上對響應時間要求較高的場合,儘量少用多線程,尤其是服務線程需要等待任務線程的場合(很多重大事故就是和這個息息相關),如果一定要用,可以對服務線程設置一個最大等待時間。

常見做法

如果單機的處理能力可以滿足實際業務的需求,那麼儘可能地使用單機多線程的處理方式,減少複雜性;反之,則需要使用多機多線程的方式。

對於單機多線程,可以引入線程池的機制,作用有二:

  • 提高性能,節省線程創建和銷燬的開銷
  • 限流,給線程池一個固定的容量,達到這個容量值後再有任務進來,就進入隊列進行排隊,保障機器極限壓力下的穩定處理能力在使用JDK自帶的線程池時,一定要仔細理解構造方法的各個參數的含義,如core pool size、max pool size、keepAliveTime、worker queue等,在理解的基礎上通過不斷地測試調整這些參數值達到最優效果。

如果單機的處理能力不能滿足需求,這個時候需要使用多機多線程的方式。這個時候除了有多線程、線程池等機制,像RPC、心跳等網絡通信調用的機制也不可少。

七:度量系統(監控、報警、服務依賴管理)

嚴格來說,度量系統不屬於性能優化的範疇,但它是系統穩定性和性能保障的基石。

關鍵流程

如果要設計這套系統,總體來說有哪些關鍵流程需要設計呢?

  • ① 確定指標
  • ② 採集數據
  • ③ 計算數據,存儲結果
  • ④ 展現和分析

需要監控和報警哪些指標數據?需要關注哪些?

按照需求出發,主要需要二方面的指標:

  • 接口性能相關,包括單個接口和全部的QPS、響應時間、調用量(統計時間維度越細越好;最好是,既能以節點爲維度,也可以以服務集羣爲維度,來查看相關數據)。其中還涉及到服務依賴關係的管理,這個時候需要用到服務依賴管理系統
  • 單個機器節點相關,包括CPU使用率、Load值、內存佔用率、網卡流量等。如果節點是一些特殊類型的服務(比如MySQL、Redis、Tair),還可以監控這些服務特有的一些關鍵指標。

數據庫索引的原理和如何優化慢查詢

我們知道一般的應用系統,讀寫比例在10:1左右,而且插入操作和一般的更新操作很少出現性能問題,遇到最多的,也是最容易出問題的,還是一些複雜的查詢操作,所以查詢語句的優化顯然是重中之重。

導致數據查詢慢的原因有多種,如:緩存失效,在此一段時間內由於高併發訪問導致 MySQL 服務器崩潰;SQL 語句編寫問題;MySQL 服務器參數問題;硬件配置限制 MySQL 服務性能問題等。

image

一:查看 MySQL 服務器運行的狀態值

如果系統的併發請求數不高,且查詢速度慢,可以忽略該步驟直接進行 SQL 語句調優步驟。

show status

我們可以通過執行如下腳本監控 MySQL 服務器運行的狀態值

#!/bin/bash
while true
do
mysqladmin -uroot -p"密碼" ext | awk '/Queries/{q=$4}/Threads_connected/{c=$4}/Threads_running/{r=$4}END{printf("%d %d %d\n",q,c,r)}' >> status.txt
sleep 1
done

執行該腳本 24 小時,獲取 status.txt 裏的內容,再次通過 awk 計算每秒請求 MySQL 服務的次數

awk '{q=$1-last;last=$1}{printf("%d %d %d\n",q,$2,$3)}' status.txt

複製計算好的內容到 Excel 中生成圖表觀察數據週期性。

如果觀察的數據有周期性的變化,如上圖的解釋,需要修改緩存失效策略。

例如:

通過隨機數在[3,6,9] 區間獲取其中一個值作爲緩存失效時間,這樣分散了緩存失效時間,從而節省了一部分內存的消耗。

當訪問高峯期時,一部分請求分流到未失效的緩存,另一部分則訪問 MySQL 數據庫,這樣減少了 MySQL 服務器的壓力。

二:獲取需要優化的 SQL 語句

2.1 方式一:查看運行的線程

show processlist

mysql> show processlist;
+----+------+-----------+------+---------+------+----------+------------------+
| Id | User | Host      | db   | Command | Time | State    | Info             |
+----+------+-----------+------+---------+------+----------+------------------+
|  9 | root | localhost | test | Query   |    0 | starting | show processlist |
+----+------+-----------+------+---------+------+----------+------------------+
1 row in set (0.00 sec)

從返回結果中我們可以瞭解該線程執行了什麼命令/SQL 語句以及執行的時間。實際應用中,查詢的返回結果會有 N 條記錄。

其中,返回的 State 的值是我們判斷性能好壞的關鍵,其值出現如下內容,則該行記錄的 SQL 語句需要優化:

Converting HEAP to MyISAM # 查詢結果太大時,把結果放到磁盤,嚴重
Create tmp table #創建臨時表,嚴重
Copying to tmp table on disk  #把內存臨時表複製到磁盤,嚴重
locked #被其他查詢鎖住,嚴重
loggin slow query #記錄慢查詢
Sorting result #排序

2.2 方式二:開啓慢查詢日誌

在配置文件 my.cnf 中的 [mysqld] 一行下邊添加兩個參數:

slow_query_log = 1
slow_query_log_file=/var/lib/mysql/slow-query.log
long_query_time = 2

log_queries_not_using_indexes = 1

其中,slowquerylog = 1 表示開啓慢查詢;slowquerylogfile 表示慢查詢日誌存放的位置;longquerytime = 2 表示查詢 >=2 秒才記錄日誌;logqueriesnotusing_indexes = 1 記錄沒有使用索引的 SQL 語句。

注意:slowquerylog_file 的路徑不能隨便寫,否則 MySQL 服務器可能沒有權限將日誌文件寫到指定的目錄中。建議直接複製上文的路徑。

修改保存文件後,重啓 MySQL 服務。在 /var/lib/mysql/ 目錄下會創建 slow-query.log 日誌文件。連接 MySQL 服務端執行如下命令可以查看配置情況。

show variables like 'slow_query%';

show variables like 'long_query_time';

測試

mysql> select sleep(2);
+----------+
| sleep(2) |
+----------+
|        0 |
+----------+
1 row in set (2.00 sec)

打開慢查詢日誌

[root@localhost mysql]# vim /var/lib/mysql/slow-query.log
/usr/sbin/mysqld, Version: 5.7.19-log (MySQL Community Server (GPL)). started with:
Tcp port: 0  Unix socket: /var/lib/mysql/mysql.sock
Time                 Id Command    Argument
# Time: 2017-10-05T04:39:11.408964Z
# User@Host: root[root] @ localhost []  Id:     3
# Query_time: 2.001395  Lock_time: 0.000000 Rows_sent: 1  Rows_examined: 0
use test;
SET timestamp=1507178351;
select sleep(2);

我們可以看到剛纔執行了 2 秒的 SQL 語句被記錄下來了。

雖然在慢查詢日誌中記錄查詢慢的 SQL 信息,但是日誌記錄的內容密集且不易查閱。因此,我們需要通過工具將 SQL 篩選出來。

MySQL 提供 mysqldumpslow 工具對日誌進行分析。我們可以使用 mysqldumpslow --help 查看命令相關用法。

常用參數如下:

    -s:排序方式,後邊接着如下參數
        c:訪問次數
        l:鎖定時間
        r:返回記錄
        t:查詢時間
    al:平均鎖定時間
    ar:平均返回記錄書
    at:平均查詢時間
    -t:返回前面多少條的數據
    -g:翻遍搭配一個正則表達式,大小寫不敏感

案例

獲取返回記錄集最多的10個sql
mysqldumpslow -s r -t 10 /var/lib/mysql/slow-query.log

獲取訪問次數最多的10個sql
mysqldumpslow -s c -t 10 /var/lib/mysql/slow-query.log

獲取按照時間排序的前10條裏面含有左連接的查詢語句
mysqldumpslow -s t -t 10 -g "left join" /var/lib/mysql/slow-query.log

三:分析 SQL 語句

3.1 方式一:explain

參考文章
通過 EXPLAIN 淺析數據庫查詢優化方法

篩選出有問題的 SQL,我們可以使用 MySQL 提供的 explain 查看 SQL 執行計劃情況(關聯表,表查詢順序、索引使用情況等)。

用法:

explain select * from category;
返回結果:

mysql> explain select * from category;
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table    | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------+
|  1 | SIMPLE      | category | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    1 |   100.00 | NULL  |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

字段解釋:

1) id:select 查詢序列號。id相同,執行順序由上至下;id不同,id值越大優先級越高,越先被執行

2) select_type:查詢數據的操作類型,其值如下:
simple:簡單查詢,不包含子查詢或 union

primary:包含複雜的子查詢,最外層查詢標記爲該值

subquery:在 select 或 where 包含子查詢,被標記爲該值

derived:在 from 列表中包含的子查詢被標記爲該值,MySQL 會遞歸執行這些子查詢,把結果放在臨時表

union:若第二個 select 出現在 union 之後,則被標記爲該值。若 union 包含在 from 的子查詢中,外層 select 被標記爲 derived

union result:從 union 表獲取結果的 select

3) table:顯示該行數據是關於哪張表

4) partitions:匹配的分區

5) type:表的連接類型,其值,性能由高到底排列如下:

system:表只有一行記錄,相當於系統表

const:通過索引一次就找到,只匹配一行數據

eq_ref:唯一性索引掃描,對於每個索引鍵,表中只有一條記錄與之匹配。常用於主鍵或唯一索引掃描

ref:非唯一性索引掃描,返回匹配某個單獨值的所有行。用於=、< 或 > 操作符帶索引的列

range:只檢索給定範圍的行,使用一個索引來選擇行。一般使用between、>、<情況

index:只遍歷索引樹

ALL:全表掃描,性能最差

注:前5種情況都是理想情況的索引使用情況。通常優化至少到range級別,最好能優化到 ref

6) possible_keys:指出 MySQL 使用哪個索引在該表找到行記錄。如果該值爲 NULL,說明沒有使用索引,可以建立索引提高性能

7) key:顯示 MySQL 實際使用的索引。如果爲 NULL,則沒有使用索引查詢

8) key_len:表示索引中使用的字節數,通過該列計算查詢中使用的索引的長度。在不損失精確性的情況下,長度越短越好 顯示的是索引字段的最大長度,並非實際使用長度

9) ref:顯示該表的索引字段關聯了哪張表的哪個字段

10) rows:根據表統計信息及選用情況,大致估算出找到所需的記錄或所需讀取的行數,數值越小越好

11) filtered:返回結果的行數佔讀取行數的百分比,值越大越好

12) extra:包含不合適在其他列中顯示但十分重要的額外信息,常見的值如下:

using filesort:說明 MySQL 會對數據使用一個外部的索引排序,而不是按照表內的索引順序進行讀取。出現該值,應該優化 SQL

using temporary:使用了臨時表保存中間結果,MySQL 在對查詢結果排序時使用臨時表。常見於排序 order by 和分組查詢 group by。出現該值,應該優化 SQL

using index:表示相應的 select 操作使用了覆蓋索引,避免了訪問表的數據行,效率不錯

using where:where 子句用於限制哪一行

using join buffer:使用連接緩存

distinct:發現第一個匹配後,停止爲當前的行組合搜索更多的行

注意:出現前 2 個值,SQL 語句必須要優化。


3.2 方式二:profiling

使用 profiling 命令可以瞭解 SQL 語句消耗資源的詳細信息(每個執行步驟的開銷)。

3.2.1 查看 profile 開啓情況

select @@profiling;

mysql> select @@profiling;
+-------------+
| @@profiling |
+-------------+
|           0 |
+-------------+
1 row in set, 1 warning (0.00 sec)

0 表示關閉狀態,1 表示開啓

3.2.2 啓用 profile

set profiling = 1;  

mysql> set profiling = 1;  
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> select @@profiling;
+-------------+
| @@profiling |
+-------------+
|           1 |
+-------------+
1 row in set, 1 warning (0.00 sec)

在連接關閉後,profiling 狀態自動設置爲關閉狀態。

3.2.3 查看執行的 SQL 列表

show profiles;

mysql> show profiles;
+----------+------------+------------------------------+
| Query_ID | Duration   | Query                        |
+----------+------------+------------------------------+
|        1 | 0.00062925 | select @@profiling           |
|        2 | 0.00094150 | show tables                  |
|        3 | 0.00119125 | show databases               |
|        4 | 0.00029750 | SELECT DATABASE()            |
|        5 | 0.00025975 | show databases               |
|        6 | 0.00023050 | show tables                  |
|        7 | 0.00042000 | show tables                  |
|        8 | 0.00260675 | desc role                    |
|        9 | 0.00074900 | select name,is_key from role |
+----------+------------+------------------------------+
9 rows in set, 1 warning (0.00 sec)


該命令執行之前,需要執行其他 SQL 語句纔有記錄

3.2.4 查詢指定 ID 的執行詳細信息

show profile for query Query_ID;

mysql> show profile for query 9;
+----------------------+----------+
| Status               | Duration |
+----------------------+----------+
| starting             | 0.000207 |
| checking permissions | 0.000010 |
| Opening tables       | 0.000042 |
| init                 | 0.000050 |
| System lock          | 0.000012 |
| optimizing           | 0.000003 |
| statistics           | 0.000011 |
| preparing            | 0.000011 |
| executing            | 0.000002 |
| Sending data         | 0.000362 |
| end                  | 0.000006 |
| query end            | 0.000006 |
| closing tables       | 0.000006 |
| freeing items        | 0.000011 |
| cleaning up          | 0.000013 |
+----------------------+----------+
15 rows in set, 1 warning (0.00 sec)

每行都是狀態變化的過程以及它們持續的時間。Status 這一列和 show processlist 的 State 是一致的。因此,需要優化的注意點與上文描述的一樣。

3.2.5 獲取 CPU、 Block IO 等信息

show profile block io,cpu for query Query_ID;

show profile cpu,block io,memory,swaps,context switches,source for query Query_ID;

show profile all for query Query_ID;

四:優化方式

主要以查詢優化、索引使用和表結構設計方面進行講解。

4.1 查詢優化

避免 SELECT *,需要什麼數據,就查詢對應的字段。

小表驅動大表,即小的數據集驅動大的數據集。如:以 A,B 兩表爲例,兩表通過 id 字段進行關聯。

當 B 表的數據集小於 A 表時,用 in 優化 exist;使用 in ,兩表執行順序是先查 B 表,再查 A 表


select * from A where id in (select id from B)
當 A 表的數據集小於 B 表時,用 exist 優化 in;使用 exists,兩表執行順序是先查 A 表,再查 B 表

select * from A where exists (select 1 from B where B.id = A.id)
一些情況下,可以使用連接代替子查詢,因爲使用 join,MySQL 不會在內存中創建臨時表。

適當添加冗餘字段,減少表關聯。

合理使用索引(下文介紹)。如:爲排序、分組字段建立索引,避免 filesort 的出現。

4.2 索引使用

4.2.1 適合使用索引的場景

  • 主鍵自動創建唯一索引
  • 頻繁作爲查詢條件的字段
  • 查詢中與其他表關聯的字段
  • 查詢中排序的字段
  • 查詢中統計或分組字段

4.2.2 不適合使用索引的場景

  • 頻繁更新的字段
  • where 條件中用不到的字段
  • 表記錄太少
  • 經常增刪改的表
  • 字段的值的差異性不大或重複性高

4.2.3 索引創建和使用原則

  • 單表查詢:哪個列作查詢條件,就在該列創建索引
  • 多表查詢:left join 時,索引添加到右表關聯字段;right join 時,索引添加到左表關聯字段
  • 不要對索引列進行任何操作(計算、函數、類型轉換)
  • 索引列中不要使用 !=,<> 非等於
  • 索引列不要爲空,且不要使用 is null 或 is not null 判斷
  • 索引字段是字符串類型,查詢條件的值要加''單引號,避免底層類型自動轉換

4.3 數據庫表結構設計

4.3.1 選擇合適的數據類型

  • 使用可以存下數據最小的數據類型
  • 使用簡單的數據類型。int 要比 varchar 類型在mysql處理簡單
  • 儘量使用 tinyint、smallint、mediumint 作爲整數類型而非 int
  • 儘可能使用 not null 定義字段,因爲 null 佔用4字節空間
  • 儘量少用 text 類型,非用不可時最好考慮分表
  • 儘量使用 timestamp 而非 datetime
  • 單表不要有太多字段,建議在 20 以內

4.3.2 表的拆分

當數據庫中的數據非常大時,查詢優化方案也不能解決查詢速度慢的問題時,我們可以考慮拆分表,讓每張表的數據量變小,從而提高查詢效率。

垂直拆分:將表中多個列分開放到不同的表中。例如用戶表中一些字段經常被訪問,將這些字段放在一張表中,另外一些不常用的字段放在另一張表中。插入數據時,使用事務確保兩張表的數據一致性。

水平拆分:按照行進行拆分。例如用戶表中,使用用戶ID,對用戶ID取10的餘數,將用戶數據均勻的分配到0~9的10個用戶表中。查找時也按照這個規則查詢數據。

4.3.3 讀寫分離

一般情況下對數據庫而言都是“讀多寫少”。換言之,數據庫的壓力多數是因爲大量的讀取數據的操作造成的。我們可以採用數據庫集羣的方案,使用一個庫作爲主庫,負責寫入數據;其他庫爲從庫,負責讀取數據。這樣可以緩解對數據庫的訪問壓力。

五:小結

b+樹性質

1.我們知道IO次數取決於b+數的高度h,假設當前數據表的數據爲N,每個磁盤塊的數據項的數量是m,則有h=㏒(m+1)N,當數據量N一定的情況下,m越大,h越小;而m = 磁盤塊的大小 / 數據項的大小,磁盤塊的大小也就是一個數據頁的大小,是固定的,如果數據項佔的空間越小,數據項的數量越多,樹的高度越低。

這就是爲什麼每個數據項,即索引字段要儘量的小,比如int佔4字節,要比bigint8字節少一半。這也是爲什麼b+樹要求把真實的數據放到葉子節點而不是內層節點,一旦放到內層節點,磁盤塊的數據項會大幅度下降,導致樹增高。當數據項等於1時將會退化成線性表。

2.當b+樹的數據項是複合的數據結構,比如(name,age,sex)的時候,b+數是按照從左到右的順序來建立搜索樹的,比如當(張三,20,F)這樣的數據來檢索的時候,b+樹會優先比較name來確定下一步的所搜方向,如果name相同再依次比較age和sex,最後得到檢索的數據;但當(20,F)這樣的沒有name的數據來的時候,b+樹就不知道下一步該查哪個節點,因爲建立搜索樹的時候name就是第一個比較因子,必須要先根據name來搜索才能知道下一步去哪裏查詢。比如當(張三,F)這樣的數據來檢索時,b+樹可以用name來指定搜索方向,但下一個字段age的缺失,所以只能把名字等於張三的數據都找到,然後再匹配性別是F的數據了, 這個是非常重要的性質,即索引的最左匹配特性。

建索引的幾大原則

七字口訣就是:
模 型 數 空 運 最 快

  • 模:模糊查詢的意思。like的模糊查詢以%開頭,索引失效。

  • 型:代表數據類型。類型錯誤,如字段類型爲varchar,where條件用number,索引也會失效。

比如:
SELECT * FROM `user` WHERE height= 180; 
height爲varchar類型導致索引失效。
  • 數:是函數的意思。對索引的字段使用內部函數,索引也會失效。
這種情況下應該建立基於函數的索引。比如:
SELECT * FROM `user` WHERE DATE(create_time) = '2022-08-26'; 
create_time字段設置索引,那就無法使用函數,否則索引失效。
  • 空:是Null的意思。索引不存儲空值,如果不限制索引列是not null,數據庫會認爲索引列有可能存在空值,所以不會按照索引進行計算。

  • 運:是運算的意思。對索引列進行(+,-,*,/,!, !=, <>)等運算,會導致索引失效。

  • 最:是最左原則。在複合索引中索引列的順序至關重要。如果不是按照索引的最左列開始查找,則無法使用索引。

  • 快:全表掃描更快的意思。如果數據庫預計使用全表掃描要比使用索引快,則不使用索引。

慢查詢優化基本步驟

  • 0.先運行看看是否真的很慢,注意設置SQL_NO_CACHE
  • 1.where條件單表查,鎖定最小返回記錄表。
  • 2.explain查看執行計劃,是否與1預期一致(從鎖定記錄較少的表開始查詢)
  • 3.order by limit 形式的sql語句讓排序的表優先查
  • 4.瞭解業務方使用場景
  • 5.加索引時參照建索引的幾大原則
  • 6.觀察結果,不符合預期繼續從0分析
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章