作為一個(gè)寫業(yè)務(wù)代碼的 "JAVA CURD BOY" ,具備寫出高效率SQL讓應(yīng)用高性能訪問(wèn)數(shù)據(jù)庫(kù)的能力非常重要。獲得這個(gè)能力的過(guò)程我收獲了點(diǎn)知識(shí)和經(jīng)驗(yàn),今天在這里分享出來(lái),希望大家多多交流指點(diǎn)。
本文內(nèi)容主要包括以下幾個(gè)方面:分析查詢SQL,MySQL查詢優(yōu)化器、數(shù)據(jù)庫(kù)存儲(chǔ)結(jié)構(gòu)、索引,索引維護(hù),索引設(shè)計(jì),SQL優(yōu)化,表結(jié)構(gòu)設(shè)計(jì),分庫(kù)分表,查詢功能架構(gòu)設(shè)計(jì)。
(資料圖片僅供參考)
MySQL提供了一個(gè)性能分析工具 EXPLAIN ,它可以幫助我們了解SQL語(yǔ)句的執(zhí)行計(jì)劃,分析查詢效率低下的原因(eg:是否使用索引,是否做全表掃描...),進(jìn)而有針對(duì)性地對(duì)SQL進(jìn)行優(yōu)化。
EXPLAIN 使用起來(lái)很簡(jiǎn)單,在你的SQL查詢語(yǔ)句前加上它就可以了。示例如下圖所示:
EXPLAIN 命令執(zhí)行之后,顯示的結(jié)果一共有12列,這里我簡(jiǎn)單說(shuō)一下各個(gè)參數(shù)的意思:
1. id:是一個(gè)查詢序列號(hào)。
2. select_type:查詢類型。
3. table:表示與查詢結(jié)果相關(guān)的表的名稱。
4. partition:表示查詢?cè)L問(wèn)的分區(qū)。
5. key:表示優(yōu)化器最終決定使用的索引是什么。
6. key_len:表示優(yōu)化器選擇的索引字段按字節(jié)計(jì)算的長(zhǎng)度。如果沒(méi)有使用索引,這個(gè)值就是空。
7. ref:列與索引的比較。
8. rows:表示為了得到查詢結(jié)果,必須掃描多少行記錄。
9. filtered:表示查詢篩選出的記錄占全部表記錄數(shù)的百分比。
10. possible_key:表示查詢時(shí)可能使用的索引。如果這里的值是空,就說(shuō)明沒(méi)有合適的索引可用。
11. Extra:表示MySQL執(zhí)行查詢中的附加信息。
12. type:表的連接類型。
我們還可以通過(guò)命令 EXPLAIN的FORMAT=json 和 FORMAT=tree 來(lái)查看SQL 執(zhí)行成本。
EXPLAIN FORMAT=json SELECT * FROM USER_TASK_STATUS_LOG WHERE userId = "1" and createTime = "2023-01-13 20:46:27";{ "query_block": { "select_id": 1, "cost_info": { "query_cost": "409960.06" }, "table": { "table_name": "USER_TASK_STATUS_LOG", "access_type": "ALL", "rows_examined_per_scan": 3990545, "rows_produced_per_join": 39905, "filtered": "1.00", "cost_info": { "read_cost": "405969.52", "eval_cost": "3990.55", "prefix_cost": "409960.06", "data_read_per_join": "11M" }, "used_columns": [ "id", "userId", "taskStatus", "createTime", "updateTime" ], "attached_condition": "((`ds`.`user_task_status_log`.`createTime` = TIMESTAMP"2023-01-13 20:46:27") and (`ds`.`user_task_status_log`.`userId` = "1"))" } }
從第 14 行開(kāi)始,其中:
read_cost 表示就是從 InnoDB 存儲(chǔ)引擎讀取的開(kāi)銷;eval_cost 表示 Server 層的 CPU 成本;prefix_cost 表示這條 SQL 的總成本;data_read_per_join 表示總的讀取記錄的字節(jié)數(shù)。如果你不了解這些參數(shù)的意義,或者說(shuō)不清楚MySQL為什么要計(jì)算這些執(zhí)行開(kāi)銷,那么接著往下看MySQL查詢優(yōu)化器。
先上一張MySQL執(zhí)行過(guò)程圖,我們來(lái)看看查詢優(yōu)化器在MySQL執(zhí)行過(guò)程中的位置。
從上圖可以看出,MySQL 數(shù)據(jù)庫(kù)由 Server層和 Engine層兩部分
Server 層負(fù)責(zé)“邏輯處理”,包括連接器、分析器、優(yōu)化器、執(zhí)行器等,涵蓋 MySQL 的大多數(shù)核心服務(wù)功能,以及所有的內(nèi)置函數(shù)(如日期、時(shí)間、數(shù)學(xué)和加密函數(shù)等),所有跨存儲(chǔ)引擎的功能都在這一層實(shí)現(xiàn),比如存儲(chǔ)過(guò)程、觸發(fā)器、視圖等。
而存儲(chǔ)引擎層負(fù)責(zé)數(shù)據(jù)的存儲(chǔ)和提取??刹灏问郊軜?gòu),支持 InnoDB、MyISAM、Memory 等多個(gè)存儲(chǔ)引擎?,F(xiàn)在最常用的存儲(chǔ)引擎是 InnoDB,它從 MySQL 5.5.5 版本開(kāi)始成為了默認(rèn)存儲(chǔ)引擎。
查詢優(yōu)化器的目標(biāo)是生成最佳的執(zhí)行計(jì)劃,而生成最佳執(zhí)行計(jì)劃的策略通常有以下兩種方式。
第一種是基于規(guī)則的優(yōu)化器(RBO,Rule-Based Optimizer),規(guī)則就是人們以往的經(jīng)驗(yàn),或者是采用已經(jīng)被證明是有效的方式。通過(guò)在優(yōu)化器里面嵌入規(guī)則,對(duì)查詢SQL進(jìn)行重寫。例如,對(duì)條件表達(dá)式進(jìn)行等價(jià)謂詞重寫、條件簡(jiǎn)化,對(duì)視圖進(jìn)行重寫,對(duì)子查詢進(jìn)行優(yōu)化,對(duì)連接語(yǔ)義進(jìn)行了外連接消除、嵌套連接消除等。
第二種是基于代價(jià)的優(yōu)化器(CBO,Cost-Based Optimizer,基于成本的優(yōu)化器),SQL 優(yōu)化器會(huì)分析所有可能的執(zhí)行計(jì)劃,選擇成本最低的執(zhí)行,
在 MySQL中,一條 SQL 的計(jì)算成本計(jì)算如下所示:
Cost = Server Cost + Engine Cost = CPU Cost + IO Cost
CPU Cost 表示計(jì)算的開(kāi)銷,比如索引鍵值的比較、記錄值的比較、結(jié)果集的排序等等。
IO Cost 表示引擎層 IO 的開(kāi)銷,MySQL 8.0 可以通過(guò)區(qū)分一張表的數(shù)據(jù)是否在內(nèi)存中,分別計(jì)算讀取內(nèi)存 IO 開(kāi)銷以及讀取磁盤 IO 的開(kāi)銷。
Server Cost 和 Engine Cost 則記錄了對(duì)于各種成本的計(jì)算。
Server Cost 記錄了 Server 層優(yōu)化器各種操作的成本。
Engine Cost 記錄了存儲(chǔ)引擎層各種操作的成本。
如果想深入了解這里有篇論文可以幫到你?!禔ccess Path Selection-in a Relational Database Management System》
記錄是按照行來(lái)存儲(chǔ)的,但是數(shù)據(jù)庫(kù)的讀取并不以行為單位,否則一次讀?。ㄒ簿褪且淮蜪/O操作)只能處理一行數(shù)據(jù),效率會(huì)非常低。因此InnoDB將數(shù)據(jù)劃分為若干個(gè)頁(yè)面,不論讀一行,還是讀多行,都是將這些行所在的頁(yè)進(jìn)行加載。也就是說(shuō),數(shù)據(jù)庫(kù)管理存儲(chǔ)空間的基本單位是頁(yè)(Page)。
一個(gè)頁(yè)中可以存儲(chǔ)多個(gè)行記錄(Row),同時(shí)在數(shù)據(jù)庫(kù)中,還存在著區(qū)(Extent)、段(Segment)和表空間(Tablespace)。行、頁(yè)、區(qū)、段、表空間的關(guān)系如下圖所示:
區(qū)(Extent)是比頁(yè)大一級(jí)的存儲(chǔ)結(jié)構(gòu),在InnoDB存儲(chǔ)引擎中,一個(gè)區(qū)會(huì)分配64個(gè)連續(xù)的頁(yè)。因?yàn)镮nnoDB中的頁(yè)大小默認(rèn)是16KB,所以一個(gè)區(qū)的大小是64*16KB=1MB。
段(Segment)由一個(gè)或多個(gè)區(qū)組成,區(qū)在文件系統(tǒng)是一個(gè)連續(xù)分配的空間(在InnoDB中是連續(xù)的64個(gè)頁(yè)),不過(guò)在段中不要求區(qū)與區(qū)之間是相鄰的。段是數(shù)據(jù)庫(kù)中的分配單位,不同類型的數(shù)據(jù)庫(kù)對(duì)象以不同的段形式存在。當(dāng)我們創(chuàng)建數(shù)據(jù)表、索引的時(shí)候,就會(huì)相應(yīng)創(chuàng)建對(duì)應(yīng)的段,比如創(chuàng)建一張表時(shí)會(huì)創(chuàng)建一個(gè)表段,創(chuàng)建一個(gè)索引時(shí)會(huì)創(chuàng)建一個(gè)索引段。
表空間(Tablespace)是一個(gè)邏輯容器,表空間存儲(chǔ)的對(duì)象是段,在一個(gè)表空間中可以有一個(gè)或多個(gè)段,但是一個(gè)段只能屬于一個(gè)表空間。數(shù)據(jù)庫(kù)由一個(gè)或多個(gè)表空間組成,表空間從管理上可以劃分為系統(tǒng)表空間、用戶表空間、撤銷表空間、臨時(shí)表空間等。
頁(yè)(Page)如果按類型劃分的話,常見(jiàn)的有數(shù)據(jù)頁(yè)(保存B+樹(shù)節(jié)點(diǎn))、系統(tǒng)頁(yè)、Undo頁(yè)和事務(wù)數(shù)據(jù)頁(yè)等。數(shù)據(jù)頁(yè)是我們最常使用的頁(yè)。
數(shù)據(jù)頁(yè)包括七個(gè)部分,分別是文件頭(File Header)、頁(yè)頭(Page Header)、最大最小記錄(Infimum+supremum)、用戶記錄(User Records)、空閑空間(Free Space)、頁(yè)目錄(Page Directory)和文件尾(File Tailer)。
頁(yè)結(jié)構(gòu)的示意圖如下所示:
實(shí)際上,我們可以把這7個(gè)數(shù)據(jù)頁(yè)分成3個(gè)部分。
首先是文件通用部分,也就是文件頭和文件尾。它們類似集裝箱,將頁(yè)的內(nèi)容進(jìn)行封裝,通過(guò)文件頭和文件尾校驗(yàn)的方式來(lái)確保頁(yè)的傳輸是完整的。
在文件頭中有兩個(gè)字段,分別是FIL_PAGE_PREV和FIL_PAGE_NEXT,它們的作用相當(dāng)于指針,分別指向上一個(gè)數(shù)據(jù)頁(yè)和下一個(gè)數(shù)據(jù)頁(yè)。連接起來(lái)的頁(yè)相當(dāng)于一個(gè)雙向的鏈表,如下圖所示:
需要說(shuō)明的是采用鏈表的結(jié)構(gòu)讓數(shù)據(jù)頁(yè)之間不需要是物理上的連續(xù),而是邏輯上的連續(xù)。
第二個(gè)部分是記錄部分,頁(yè)的主要作用是存儲(chǔ)記錄,所以“最小和最大記錄”和“用戶記錄”部分占了頁(yè)結(jié)構(gòu)的主要空間。另外空閑空間是個(gè)靈活的部分,當(dāng)有新的記錄插入時(shí),會(huì)從空閑空間中進(jìn)行分配用于存儲(chǔ)新記錄,如下圖所示:
第三部分是索引部分,這部分重點(diǎn)指的是頁(yè)目錄,它起到了記錄的索引作用。因?yàn)樵陧?yè)中,記錄是以單向鏈表的形式進(jìn)行存儲(chǔ)的。單向鏈表的特點(diǎn)就是插入、刪除非常方便,但是檢索效率不高,最差的情況下需要遍歷鏈表上的所有節(jié)點(diǎn)才能完成檢索,因此在頁(yè)目錄中提供了二分查找的方式,用來(lái)提高記錄的檢索效率。
索引的出現(xiàn)是為了提高數(shù)據(jù)查詢的效率,就像書的目錄一樣。沒(méi)有索引,我們就只能一頁(yè)一頁(yè)去找。而加上索引之后,我們可以根據(jù)目錄來(lái)快速查找我們所需要的內(nèi)容。
對(duì)于數(shù)據(jù)庫(kù)的表而言,索引就是它的“目錄”,它是幫助MySQL系統(tǒng)快速檢索數(shù)據(jù)的一種數(shù)據(jù)結(jié)構(gòu)。索引在插入時(shí)會(huì)對(duì)數(shù)據(jù)進(jìn)行排序,我們可以在索引中按照查詢條件,檢索索引字段的值,然后快速定位數(shù)據(jù)記錄的位置,這樣就不需要遍歷整個(gè)數(shù)據(jù)表了。
可以用于提高讀寫效率的數(shù)據(jù)結(jié)構(gòu)很多,那么如何評(píng)價(jià)一個(gè)索引數(shù)據(jù)結(jié)構(gòu)的好壞呢?
數(shù)據(jù)庫(kù)服務(wù)器有兩種存儲(chǔ)介質(zhì),分別為硬盤和內(nèi)存。硬盤相當(dāng)于永久存儲(chǔ)介質(zhì)。內(nèi)存屬于臨時(shí)存儲(chǔ),容量有限,當(dāng)發(fā)生意外時(shí)(比如斷電或者發(fā)生故障重啟)會(huì)造成數(shù)據(jù)丟失。
所以盡管內(nèi)存的讀取速度很快,但我們還是需要將索引存放到硬盤上。這樣的話,當(dāng)我們?cè)谟脖P上進(jìn)行查詢時(shí),也就產(chǎn)生了硬盤的I/O操作,而硬盤I/O耗時(shí)是比較高的。
所以好的索引數(shù)據(jù)結(jié)構(gòu)應(yīng)該盡量減少硬盤的I/O操作,降低耗時(shí)。
MySQL默認(rèn)存儲(chǔ)引擎是InnoDB存儲(chǔ)引擎,InnoDB存儲(chǔ)引擎使用的是B+樹(shù)索引。
B樹(shù)的英文是Balance Tree,也就是平衡的多路搜索樹(shù)。在文件系統(tǒng)和數(shù)據(jù)庫(kù)系統(tǒng)中的索引結(jié)構(gòu)經(jīng)常采用B樹(shù)來(lái)實(shí)現(xiàn)。
B樹(shù)的結(jié)構(gòu)如下圖所示(一棵3階的B樹(shù)):
相較于B樹(shù)而言,B+樹(shù)在兩個(gè)方面做出了改進(jìn)和提升,一方面是查詢的穩(wěn)定性,另一方面是查詢的效率更高。
下圖是一棵3階的B+樹(shù)
B+樹(shù)和B樹(shù)有個(gè)根本的差異在于,B+樹(shù)的中間節(jié)點(diǎn)并不直接存儲(chǔ)數(shù)據(jù)。這樣的好處都有什么呢?
首先,B+樹(shù)查詢效率更穩(wěn)定。因?yàn)锽+樹(shù)每次只有訪問(wèn)到葉子節(jié)點(diǎn)才能找到對(duì)應(yīng)的數(shù)據(jù),而在B樹(shù)中,非葉子節(jié)點(diǎn)也會(huì)存儲(chǔ)數(shù)據(jù),這樣就會(huì)造成查詢效率不穩(wěn)定的情況,有時(shí)候訪問(wèn)到了非葉子節(jié)點(diǎn)就可以找到關(guān)鍵字,而有時(shí)需要訪問(wèn)到葉子節(jié)點(diǎn)才能找到關(guān)鍵字。
其次,B+樹(shù)的查詢效率更高,這是因?yàn)橥ǔ+樹(shù)比B樹(shù)更矮胖(階數(shù)更大,深度更低),查詢所需要的磁盤I/O也會(huì)更少。同樣的磁盤頁(yè)大小,B+樹(shù)可以存儲(chǔ)更多的節(jié)點(diǎn)關(guān)鍵字。
不僅是對(duì)單個(gè)關(guān)鍵字的查詢上,在查詢范圍上,B+樹(shù)的效率也比B樹(shù)高。這是因?yàn)樗嘘P(guān)鍵字都出現(xiàn)在B+樹(shù)的葉子節(jié)點(diǎn)中,并通過(guò)有序鏈表進(jìn)行了鏈接。而在B樹(shù)中則需要通過(guò)中序遍歷才能完成查詢范圍的查找,效率要低很多。
如果通過(guò)B+樹(shù)的索引查詢行記錄,首先是從B+樹(shù)的根開(kāi)始,逐層檢索,直到找到葉子節(jié)點(diǎn),也就是找到對(duì)應(yīng)的數(shù)據(jù)頁(yè)為止,將數(shù)據(jù)頁(yè)加載到內(nèi)存中,頁(yè)目錄中的槽(slot)采用二分查找的方式先找到一個(gè)粗略的記錄分組,然后再在分組中通過(guò)鏈表遍歷的方式查找記錄。
B+樹(shù)為了維護(hù)索引有序性,在插入新值的時(shí)候需要做必要的維護(hù)。這里存在兩種不同數(shù)據(jù)類型的插入情況。
數(shù)據(jù)順序(或逆序)插入: B+ 樹(shù)索引的維護(hù)代價(jià)非常小,葉子節(jié)點(diǎn)都是從左往右進(jìn)行插入,比較典型的是自增 ID 的插入、時(shí)間的插入(若在自增 ID 上創(chuàng)建索引,時(shí)間列上創(chuàng)建索引,則 B+ 樹(shù)插入通常是比較快的)。數(shù)據(jù)無(wú)序插入: B+ 樹(shù)為了維護(hù)排序,需要對(duì)頁(yè)進(jìn)行分裂、旋轉(zhuǎn)等開(kāi)銷較大的操作,另外,即便對(duì)于固態(tài)硬盤,隨機(jī)寫的性能也不如順序?qū)懀源疟P性能也會(huì)收到較大影響。比較典型的是用戶昵稱,每個(gè)用戶注冊(cè)時(shí),昵稱是隨意取的,若在昵稱上創(chuàng)建索引,插入是無(wú)序的,索引維護(hù)需要的開(kāi)銷會(huì)比較大。所以對(duì)于 B+ 樹(shù)索引,在 MySQL 數(shù)據(jù)庫(kù)設(shè)計(jì)中,建議主鍵的索引設(shè)計(jì)為順序,比如使用自增,或使用函數(shù) UUID_TO_BIN 排序的 UUID,而不用無(wú)序值做主鍵。
MySQL InnoDB 存儲(chǔ)引擎是索引組織表的存儲(chǔ)方式。
在索引組織表中,數(shù)據(jù)即索引,索引即數(shù)據(jù),數(shù)據(jù)根據(jù)主鍵排序存放在索引中。
索引組織表示例:
表 User 的主鍵是 id,所以表中的數(shù)據(jù)根據(jù) id 排序存儲(chǔ),葉子節(jié)點(diǎn)存放了表中完整的記錄,可以看到表中的數(shù)據(jù)存放在索引中,即表就是索引,索引就是表。
InnoDB 存儲(chǔ)引擎的數(shù)據(jù)是根據(jù)主鍵索引排序存儲(chǔ)的,除了主鍵索引(聚集索引)外,其他的索引都稱之為二級(jí)索引, 或非聚集索引。
二級(jí)索引也是一顆 B+ 樹(shù)索引,但它和主鍵索引不同的是葉子節(jié)點(diǎn)存放的是索引鍵值、主鍵值。
通過(guò)二級(jí)索引 idx_name 只能定位主鍵值,需要額外再通過(guò)主鍵索引進(jìn)行查詢,才能得到最終的結(jié)果。這種“二級(jí)索引通過(guò)主鍵索引進(jìn)行再一次查詢”的操作叫作“回表”,你可以通過(guò)下圖理解二級(jí)索引的查詢:
索引組織表這樣的二級(jí)索引設(shè)計(jì)有一個(gè)非常大的好處:若記錄發(fā)生了修改,則其他索引無(wú)須進(jìn)行維護(hù),除非記錄的主鍵發(fā)生了修改。
與堆表的索引實(shí)現(xiàn)對(duì)比著看,你會(huì)發(fā)現(xiàn)索引組織表在存在大量變更的場(chǎng)景下,性能優(yōu)勢(shì)會(huì)非常明顯,因?yàn)榇蟛糠智闆r下都不需要維護(hù)其他二級(jí)索引。
覆蓋索引是select的數(shù)據(jù)列只用從索引中就能夠取得,不必讀取數(shù)據(jù)行,換句話說(shuō)查詢列要被所建的索引覆蓋。
由于覆蓋索引可以減少樹(shù)的搜索次數(shù),顯著提升查詢性能,所以使用覆蓋索引是一個(gè)常用的性能優(yōu)化手段。
當(dāng)然,索引字段的維護(hù)總是有代價(jià)的。因此,在建立冗余索引來(lái)支持覆蓋索引時(shí)就需要權(quán)衡考慮了。
函數(shù)索引即索引鍵是一個(gè)函數(shù)表達(dá)式。
舉個(gè)例子,假設(shè)User表中創(chuàng)建了register_date索引。
SELECT * FROM User WHERE DATE_FORMAT(register_date,"%Y-%m") = "2021-01"
這條 SQL 因?yàn)樗饕?register_date 只對(duì) register_date 的數(shù)據(jù)排序,沒(méi)有對(duì)DATE_FORMAT(register_date) 排序,
因此上述 SQL 無(wú)法使用二級(jí)索引register_date。此時(shí)我們可以創(chuàng)建一個(gè)DATE_FORMAT(register_date) 索引,來(lái)提升SQL的查詢性能。
ALTER TABLE User ADD INDEX func_register_date((DATE_FORMAT(register_date,"%Y-%m")));
MySQL是支持前綴索引的,也就是說(shuō),你可以定義字符串的一部分作為索引。從而創(chuàng)建占用空間更小、查詢效率相同的字段。
示例代碼:
alter table user add index user_uuid_index(user_uuid(10));
組合索引的多個(gè)字段是有序的,遵循左對(duì)齊的原則。假設(shè)我們創(chuàng)建一個(gè)組合索引,排序的方式是sex、age和height。此時(shí),篩選的條件也要遵循從左向右的原則。如果中斷,那么,斷點(diǎn)后面的條件就沒(méi)有辦法利用索引了。
假設(shè)查詢條件為 "WHERE sex = "男" AND age = 25 AND height = 180",包含了從左到右的所有字段,所以可以最大限度使用全部組合索引。
假如把條件換成“WHERE age = 25 AND height = 180”,最左邊的字段 sex 沒(méi)有包含到條件當(dāng)中,中斷了,所以這個(gè)條件完全不能使用組合索引。
我之前講了索引的使用和它的底層原理,今天我來(lái)講一講索引的使用原則。既然我們的目標(biāo)是提升SQL的查詢效率,那么該如何通過(guò)索引讓效率最大化呢?
1.字段的數(shù)值有唯一性的限制,比如用戶名
索引本身可以起到約束的作用,比如唯一索引、主鍵索引都是可以起到唯一性約束的,因此在我們的數(shù)據(jù)表中,如果某個(gè)字段是唯一性的,就可以直接創(chuàng)建唯一性索引,或者主鍵索引。
2.頻繁作為WHERE查詢條件的字段,尤其在數(shù)據(jù)表大的情況下
在數(shù)據(jù)量大的情況下,某個(gè)字段在SQL查詢的WHERE條件中經(jīng)常被使用到,那么就需要給這個(gè)字段創(chuàng)建索引了。創(chuàng)建普通索引就可以大幅提升數(shù)據(jù)查詢的效率。
3.需要經(jīng)常GROUP BY和ORDER BY的列
索引就是讓數(shù)據(jù)按照某種順序進(jìn)行存儲(chǔ)或檢索,因此當(dāng)我們使用GROUP BY對(duì)數(shù)據(jù)進(jìn)行分組查詢,或者使用ORDER BY對(duì)數(shù)據(jù)進(jìn)行排序的時(shí)候,就需要對(duì)分組或者排序的字段進(jìn)行索引。
4.UPDATE、DELETE的WHERE條件列,一般也需要?jiǎng)?chuàng)建索引
先根據(jù)WHERE條件列檢索出來(lái)這條記錄,然后再對(duì)它進(jìn)行更新或刪除。如果進(jìn)行更新的時(shí)候,更新的字段是非索引字段,提升的效率會(huì)更明顯,這是因?yàn)榉撬饕侄胃虏恍枰獙?duì)索引進(jìn)行維護(hù)。
不過(guò)在實(shí)際工作中,我們也需要注意平衡,如果索引太多了,在更新數(shù)據(jù)的時(shí)候,如果涉及到索引更新,就會(huì)造成負(fù)擔(dān)。
5.DISTINCT字段需要?jiǎng)?chuàng)建索引
有時(shí)候我們需要對(duì)某個(gè)字段進(jìn)行去重,使用DISTINCT,那么對(duì)這個(gè)字段創(chuàng)建索引,也會(huì)提升查詢效率。
6.做多表JOIN連接操作時(shí),創(chuàng)建索引需要注意以下的原則
首先,連接表的數(shù)量盡量不要超過(guò)3張,因?yàn)槊吭黾右粡埍砭拖喈?dāng)于增加了一次嵌套的循環(huán),數(shù)量級(jí)增長(zhǎng)會(huì)非??欤瑖?yán)重影響查詢的效率。
其次,對(duì)WHERE條件創(chuàng)建索引,因?yàn)閃HERE才是對(duì)數(shù)據(jù)條件的過(guò)濾。如果在數(shù)據(jù)量非常大的情況下,沒(méi)有WHERE條件過(guò)濾是非常可怕的。
最后,對(duì)用于連接的字段創(chuàng)建索引,并且該字段在多張表中的類型必須一致。
盡量使用占用存儲(chǔ)空間更少的數(shù)據(jù)類型,例如字段既可以用文本類型,也可以用整數(shù)類型時(shí),盡量使用整數(shù)類型。
需要注意的是:修改數(shù)據(jù)類型,節(jié)省存儲(chǔ)空間的同時(shí),你要考慮到數(shù)據(jù)不能超過(guò)取值范圍;
在數(shù)據(jù)量大,而且需要頻繁進(jìn)行連接的時(shí)候,為了提升效率,可以考慮增加冗余字段來(lái)減少連接。
需要注意的是:增加冗余字段的時(shí)候,不要忘了確保數(shù)據(jù)一致性;
把1個(gè)包含很多字段的表拆分成2個(gè)或者多個(gè)相對(duì)較小的表。這樣做的原因是,把這個(gè)大表拆分開(kāi),把使用頻率高的字段放在一起形成一個(gè)表,把剩下的使用頻率低的字段放在一起形成一個(gè)表,這樣查詢操作每次讀取的記錄比較小,查詢效率自然也就提高了。
需要注意的是:把大表拆分,也意味著你的查詢會(huì)增加新的連接,從而增加額外的開(kāi)銷和運(yùn)維的成本。
分庫(kù)分表的目的就是為了解決由于數(shù)據(jù)量過(guò)而導(dǎo)致數(shù)據(jù)庫(kù)性能降低的問(wèn)題,將原來(lái)獨(dú)立的數(shù)據(jù)庫(kù)拆分為若干數(shù)據(jù)庫(kù)組成,將數(shù)據(jù)大表拆分成若干數(shù)據(jù)表,使得單一數(shù)據(jù)庫(kù)、單一數(shù)據(jù)表的數(shù)據(jù)量變小,從而達(dá)到提升數(shù)據(jù)庫(kù)性能的目的。
一般來(lái)說(shuō),在系統(tǒng)設(shè)計(jì)階段就應(yīng)該根據(jù)業(yè)務(wù)耦合程度來(lái)確定用哪種分庫(kù)分表的方式(方案),在數(shù)據(jù)量及訪問(wèn)壓力不是特別大的情況,首先考慮緩存、讀寫分離、索引技術(shù)等方案。若數(shù)據(jù)量極大,且連續(xù)增長(zhǎng),再考慮水平分庫(kù)水平分表的方案。
分庫(kù)分表的方式在生產(chǎn)中通常包括:垂直分庫(kù)、垂直分表、水平分庫(kù)和水平分表四種。
定義:將一個(gè)表按字段分成多表,每個(gè)表存儲(chǔ)其中一部分字段。
帶來(lái)的提升是:
為了避免IO爭(zhēng)搶并減少鎖表的幾率。充分發(fā)揮熱門數(shù)據(jù)的操作效率。需要注意的是:拆分后,盡量從業(yè)務(wù)角度避免聯(lián)查,否則性能方面將得不償失。
為什么大字段表的IO效率低:
第一由于數(shù)據(jù)量本身大,需要更長(zhǎng)的讀取時(shí)間;
第二是跨頁(yè),頁(yè)是數(shù)據(jù)存儲(chǔ)單位,很多查找及定位操作都是以頁(yè)為單位,單頁(yè)內(nèi)的數(shù)據(jù)行越來(lái)越多數(shù)據(jù)庫(kù)整體性能越好,而大字段占用空間大,單頁(yè)內(nèi)存儲(chǔ)行數(shù)少,因此IO效率低。
第三,數(shù)據(jù)庫(kù)以行為單位將數(shù)據(jù)加載到內(nèi)存中,這樣表中字段長(zhǎng)度較短且訪問(wèn)頻率較高,內(nèi)存能加載更多的數(shù)據(jù),命中率高,減少了磁盤IO,從而提升了數(shù)據(jù)庫(kù)性能。
垂直分庫(kù)是指按照業(yè)務(wù)將表進(jìn)行分類,分布到不同的數(shù)據(jù)庫(kù)上面,每個(gè)庫(kù)可以放在不同的服務(wù)器上,從而使訪問(wèn)壓力被分?jǐn)傇诙鄠€(gè)服務(wù)器,大大提高性能,同時(shí)能提高整體架構(gòu)的業(yè)務(wù)清晰度,不同的業(yè)務(wù)庫(kù)可根據(jù)自身情況定制優(yōu)化方案,它的核心理念是專庫(kù)專用。
帶來(lái)的提升是:
解決業(yè)務(wù)層面的耦合,業(yè)務(wù)清晰能對(duì)不同業(yè)務(wù)的數(shù)據(jù)進(jìn)行分級(jí)管理、維護(hù)、監(jiān)控和擴(kuò)展等高并發(fā)場(chǎng)景下,垂直分庫(kù)一定程度上提升IO、數(shù)據(jù)庫(kù)連接和降低單機(jī)硬件資源的瓶頸水平分庫(kù)就是把同一個(gè)表的數(shù)據(jù)按一定規(guī)則拆到不同的數(shù)據(jù)庫(kù)中,每個(gè)庫(kù)可以放在不同的服務(wù)器上。
帶來(lái)的提升是:
解決了單庫(kù)大數(shù)據(jù),高并發(fā)的性能瓶頸。提高了系統(tǒng)的穩(wěn)定性和可用性。需要注意的是:使用水平分庫(kù)不僅需要解決跨庫(kù)帶來(lái)的問(wèn)題,還需要解決數(shù)據(jù)路由的問(wèn)題。
水平分表就是在同一個(gè)數(shù)據(jù)庫(kù)內(nèi),把同一個(gè)表的數(shù)據(jù)按一定規(guī)則拆到多個(gè)表中(對(duì)數(shù)據(jù)的拆分,不影響表結(jié)構(gòu))。
它帶來(lái)的提升是:
優(yōu)化單一表數(shù)據(jù)量過(guò)大而產(chǎn)生的性能問(wèn)題避免IO爭(zhēng)搶并減少鎖表的幾率比學(xué)習(xí)知識(shí)更重要的是靈活地調(diào)用知識(shí)儲(chǔ)備高效解決實(shí)際問(wèn)題的能力。想要提高應(yīng)用程序的查詢性能,還要結(jié)合實(shí)際的業(yè)務(wù)需求設(shè)計(jì)合理的功能架構(gòu)。
舉個(gè)簡(jiǎn)單例子來(lái)說(shuō)明一下:
假設(shè)我們現(xiàn)在需要提供一個(gè)接口供前端顯示統(tǒng)計(jì)數(shù)據(jù)。用于統(tǒng)計(jì)的數(shù)據(jù)存在一張千萬(wàn)級(jí)數(shù)據(jù)的表中,并且數(shù)據(jù)量級(jí)在快速增加。
你會(huì)怎么設(shè)計(jì)這個(gè)接口,分庫(kù)分表?
首先會(huì)想到的是設(shè)計(jì)合適的索引來(lái)加快查詢和統(tǒng)計(jì)速度。但是因?yàn)楸淼臄?shù)據(jù)級(jí)很大,如果每次統(tǒng)計(jì)都在這張表中進(jìn)行,處理耗時(shí)依然會(huì)很長(zhǎng)。
此時(shí)我們可以想辦法把統(tǒng)計(jì)表的數(shù)據(jù)縮減,例如將統(tǒng)計(jì)表的數(shù)據(jù)提前統(tǒng)計(jì)好存入“統(tǒng)計(jì)表2”,使用定時(shí)任務(wù)將統(tǒng)計(jì)表新插入的數(shù)據(jù)集也統(tǒng)計(jì)合并到“統(tǒng)計(jì)表2”,需要統(tǒng)計(jì)數(shù)據(jù)時(shí)就在這張”統(tǒng)計(jì)表2”中進(jìn)行。因?yàn)楸淼臄?shù)據(jù)量小了,所以查詢性能可以提高很多。
但是需要注意的是,這樣做會(huì)額外占用了很多存儲(chǔ)空間,并且返回的統(tǒng)計(jì)數(shù)據(jù)并不精準(zhǔn),這就要看實(shí)際業(yè)務(wù)來(lái)做取舍了。
鑫茂,深圳,Java開(kāi)發(fā)工程師,2022年3月參加工作。
喜讀思維方法、哲學(xué)心理學(xué)以及歷史等方面的書,偶爾寫些文字。
希望通過(guò)文章,結(jié)識(shí)更多同道中人。