天天看點

《大話資料庫》-SQL語句執行時,底層究竟做了什麼小動作?

大家好,我是Taoye,試圖用玩世不恭過的态度對待生活的Coder。

現如今我們已然進入了大資料時代,無論是業内還是業外的朋友,相信都有聽說過資料庫這個名詞。資料是一個項目的精華,也扮演着為企業創造價值的重要角色,一個較為完善的公司一般都會有專門的DBA來管理資料庫,以便更好的為使用者服務。

網際網路的發展速度之快,以緻大量的APP應用湧入使用者的視野,在大多數APP中都會有“推薦”這一闆塊,而這個闆塊功能的核心正是基于使用者以往的資料記錄而實作的。再如《死亡筆記》中L·Lawliet這一角色所提到的大數定律,在衆多繁雜的資料中必然存在着某種規律,偶然中必然包含着某種必然的發生。不管是我們提到的大數定律,還是最近火熱的大資料亦或其他領域都離不開大量資料的支援。

如上,我們可初步體會到資料的重要性,而要想更好的管理資料,則避免不了與資料庫打交道。對于資料庫而言,操作資料庫的使用者就相當于一位老闆,我們需要向資料庫發出指令,然後期望資料庫給我們傳回想要的結果。

我們可以想象一下這麼一個場景,老闆給Taoye釋出了這麼一個任務:“Taoye啊,你作為一位專業的闆磚工,我現在需要你在一小時的之内将工地的轉搬回來。”是的,老闆釋出任務隻注重結果和效率,而不在意你闆磚的過程。我們在執行SQL語句的時候也是如此,一般隻關心執行的結果和效率是否滿足使用者的需求。

最近,Taoye重新把之前學習資料庫時候所記錄的筆記複習了一下,然後又系統性的拜讀了丁奇大大的《MySQL實戰45講》中的内容,是以想要把MySQL系列的知識内容單獨整理出來,在進行自我提高的同時,也希望能給予大家一點幫助。

《大話資料庫》-SQL語句執行時,底層究竟做了什麼小動作?

我們要想系統性的學習MySQL資料庫,首先不得不了解MySQL的體系結構。在MySQL中,主要是由什麼功能子產品組成?每一個功能子產品在SQL語句執行的時候分别扮演了一個什麼樣的角色?MySQL的強大之處在哪,竟會受到如此之多的開發者的青睐?這些都是我們每一位學習MySQL的朋友必須了解甚至掌握的内容,以下便是MySQL資料庫的體系結構及其執行流程:

MySQL的體系結構及執行流程

從上圖,我們可以看出MySQL整體上主要分為了Server層和存儲引擎層兩個部分。

在Server層内部又包括連接配接器、緩存、分析器、優化器、執行器等功能部件,主要負責了MySQL的大多數核心服務功能,比如存儲過程、函數、觸發器、視圖等。

而存儲引擎層主要的是負責資料的存儲和提取,在MySQL中可支援MyISAM、InnoDB、Memory等多種存儲引擎,其中最常見的是InnoDB和MyISAM,這也是我們在學習存儲引擎時候的一個重點。在MySQL 5.5.5版本之前預設使用的是MyISAM,而在此版本之後預設采用的是InnoDB存儲引擎。我們在實際建立資料表的時候,也可以通過<code>ENGINE</code>來指定使用的存儲引擎,如下所示。我們可以對<code>tb_comment</code>資料表指定使用MyISAM存儲引擎:

MyISAM和InnoDB這兩種存儲引擎最主要的差別是事務以及鎖機制:

InnoDB支援事務,而MyISAM不支援事務

InnoDB一般采用的是行鎖,鎖的粒度小,開銷大,鎖表慢,但是在高并發場景下性能更好。而MyISAM采用的是表鎖,特征與行鎖相反。

關于MySQL的事務和鎖機制,這裡隻是簡單的提一下,具體的細節我們後面聊。下面我們将MySQL的體系結構中每一個功能子產品單獨的分離開來,依次看看每一個功能子產品所展現出的作用。

我們要想正常的操作資料庫,首先需要經過連接配接器這道大門。在正式引出連接配接器之前,各位看官不妨來看看下面一個例子:

金主大大成天擔心自己财産的安全,必然會将自己的money存儲在銀行金庫中。某一天金主大大要想取出一部分的财産來維持公司的營運,而銀行為了保障金庫中money的安全性,金主必然要進行身份核驗以及一系列防盜系統的檢測。

在MySQL資料庫中,連接配接器的作用其實就類似于上面的身份校驗以及防盜系統,主要是負責與用戶端建立連接配接、權限校驗、維持和管理連接配接。我們要想操作資料庫,首先需要通過賬号、密碼等資訊來連接配接資料庫,假如我們想要以root賬戶、密碼為666666來連接配接192.168.31.100:3307的MySQL服務,則可以執行以下指令:

當資料比對成功時,連接配接器就能允許使用者與資料庫建立連接配接。此外,連接配接器還需要驗證該使用者是否有權限對資料表進行操作,這個時候連接配接器會去權限表中查詢連接配接使用者的權限,隻有在具有操作權限的前提下才能操作資料表。如果我們在與資料庫已經建立連接配接的前提下,但是不對資料庫進行任何操作,這個時候連接配接就會處于空閑狀态,我們可以通過<code>show processlist</code>指令來檢視已經建立的連接配接數和處于空閑狀态的連接配接,其中command字段為sleep表示連接配接空閑:

如果連接配接長期處于空閑狀态而不做任何操作,當超過一定時間時,就會自動斷開連接配接,而這個時間門檻值主要是通過<code>wait_timeout</code>屬性來決定的,預設是28800,即8小時。<code>show variables like '%wait_timeout%'"</code>可檢視時間門檻值,<code>set @@session.wait_timeout=xxx</code>可修改目前會話下的時間門檻值,具體操作如下:

緩存這個概念,學習過《計算機組成原理》或是其他相關課程的朋友應該并不陌生,緩存一般使用的是SRAM(靜态随機存儲器)技術實作的,相較于DRAM(動态随機存儲器)而言,它最主要的優勢在于速度快,能夠大大提高資料的查詢效率。

關于緩存,我們可以來做一個簡答的計算題:

假設查詢一次緩存需要1s,查詢一次記憶體需要10s,緩存命中的機率為90%,一位使用者想要查詢100次,則使用緩存和不使用緩存的平均查詢時間是多少?

可見,根據局部性原理,緩存的存在是可以大大提高資料的查詢效率的。

《大話資料庫》-SQL語句執行時,底層究竟做了什麼小動作?

由上方的執行流程圖,我們也可以知道,用戶端通過連接配接器與MySQL伺服器建立連接配接之後,這個時候就會來到緩存中查詢使用者所需的資料。假設使用者向MySQL發出以下一條查詢語句:

MySQL收到使用者發出的請求之後,就會先到緩存中看看之前是否有執行過相同的語句,而且之前執行的結果會以key-value鍵值對的形式直接進行存儲。假如之前有執行過這條指令,則直接從緩存中取出資料并回報給使用者,如果沒有執行過,則會繼續走 分析器 -&gt; 優化器 -&gt; 執行器 -&gt; 存儲引擎這條鍊路。是以說,緩存的命中可以省去後面一系列操作所消耗的時間,這也是緩存提高查詢效率的原因。

按道理來講,緩存的引入應該是相當不錯的,而且使用者查詢次數越多就越能展現緩存的強大,但為什麼在MySQL 8.0版本之後直接舍去了緩存部件呢?

《大話資料庫》-SQL語句執行時,底層究竟做了什麼小動作?

在理想情況下,緩存引入确實是非常的完美,但是在資料庫中,我們除了查詢操作之外,還有更新操作(增、删、改)。每當我們執行過一次更新操作的時候,資料庫中的資料就已然發生了改變,而當我們再次發出指令從緩存中取出的資料就不再是使用者所期望的資料了。是以對MySQL而言,每當使用者進行更新操作時,都會清空一次緩存,然後再次重新緩存新的資料,而這個過程給MySQL帶來的壓力是很大的,也大大削弱了SQL語句的執行效率。是以說,緩存對于一些查詢多,更新少的資料表比較有用,而對那些更新比較頻繁的資料表就會适得其反。

如果使用的是MySQL 8.0以下的版本,我們可以根據實際需求來确定是否開啟緩存,主要是通過<code>my.cnf</code>配置檔案中的<code>query_cache_type</code>來決定的,0代表禁用緩存,1代表開啟緩存,2代表根據需要使用緩存。在執行SQL語句的時候可以附帶<code>SQL_CACHE和SQL_NO_CACHE</code>來确定執行時是否使用緩存,并且可以通過<code>show status like '%qcache%'</code>指令來檢視緩存的整體情況:

如果在執行查詢SQL時沒有命中緩存,則需要去資料表中查詢使用者所需的資料了,也就是開始進入分析器來對使用者發出的SQL語句進行分析。

分析的時候主要是包括預處理和解析的過程,在這個階段會解析SQL語句的語義,并對語句中的一些關鍵字和非關鍵字進行提取、解析,并組成一個解析樹交給後面的執行器執行。具體的關鍵詞包括但不限定于select/update/delete/or/in/where/group by/having/count/limit等,如果分析到文法或關鍵詞錯誤,會直接給用戶端抛出異常:<code>ERROR:You have an error in your SQL syntax.</code>

比如使用者執行:<code>select * from tb_comment where user_id=1</code>,在分析器中就會通過語義規則器将<code>select、from、where</code>等提取和比對出來,然後進行分析并校驗,假如在校驗的時候發現<code>tb_comment</code>中并不存在<code>user_id</code>字段,則會報錯:<code>Unknown column 'user_id' in 'where clause</code>

分析器對使用者的SQL語義進行分析之後,則說明SQL語句本身是沒有任何問題,并且是可以被正常執行的。此時,MySQL已經知道了使用者的意圖,使用者是想查詢還是更新,MySQL都心知肚明。

優化器主要是對SQL進行優化,會根據執行計劃進行最優的選擇來比對合适的索引,并選擇最佳的執行方案。在丁奇《MySQL實戰45講》中提到,一個語句有多表關聯(join)的時候,會決定各個表的連接配接順序。比如你執行下面這樣的語句,這個語句是執行兩個表的join:

既可以先從表t1裡面取出c=10的記錄的ID值,再根據ID值關聯到表t2,再判斷t2裡面d的值是否等于20。

也可以先從表t2裡面取出d=20的記錄的ID值,再根據ID值關聯到t1,再判斷t1裡面c的值是否等于10。

這兩種執行方式的邏輯結果是一樣的,但是執行的效率會有不同,而優化器的作用就是決定選擇使用較佳的方案來執行。為了更好的了解丁奇大大的例子,我們可以對其進行類别:我們要想在中國上海找到年齡在20-30歲之間的人。第一種方案是先鎖定上海,然後再查詢年齡;而第二種是先在全國查詢年齡區間在20-30的人,然後再鎖定上海。至于哪種方案較優,相信各位看官都明白吧。

《大話資料庫》-SQL語句執行時,底層究竟做了什麼小動作?

當然了,優化器對SQL進行優化的地方不僅僅局限于以上場景。在我們建立聯合索引,并對資料表進行查詢的時候,優化器同樣可能會對其進行優化。且看下面一個例子:

在上面,我們首先根據tb_comment資料表中的openid、comment_time、problem_id建立一個組合索引<code>oid_ctime_pid</code>,然後根據索引字段進行查詢。然而在查詢的時候,我們有意地将篩選的字段順序打亂,與索引字段的建立順序不一緻。(索引建立字段順序:openid、comment_time、problem_id,查詢條件字段順序:openid、problem_id、comment_time)但是我們采用<code>explain</code>執行計劃檢索的時候發現<code>key</code>屬性為<code>oid_ctime_pid</code>,換句話講,即使我們不遵循最左比對原則,此時依然是走了<code>oid_ctime_pid</code>組合索引的。之是以能夠達到這樣的效果,正式因為優化器的存在,在其内部對查詢條件的順序進行了優化,進而利用索引提高了資料的查詢效率。

注意:以上内容提前涉及到了資料庫當中索引的概念,索引是非常非常重要的,我們日後會單獨的詳細講講索引。另外,還有一點值得注意的是,雖然說優化器在一定程度上能夠優化SQL,但是這種優化程度僅僅是MySQL所認為的一個較佳狀态,而不一定能夠滿足DBA所需要的标準。

OK,MySQL已經對使用者發出的SQL語句進行了分析,也進行了優化,現在就要進入執行階段來執行SQL語句了。

執行器在對SQL語句進行執行的時候,會根據表的引擎定義,去使用這個引擎所提供的接口,這些接口都是引擎中已經定義好的,執行器直接通過“拿來主義”進行調用即可。引擎還有一個名字叫做“表處理器”,這個名字應該說更能展現出它所存在的意義。

我們同樣以一條簡單的SQL語句來說明下這個執行過程:

假設這張表采用的是InnoDB存儲引擎,則首先InnoDB存儲引擎接口會去查詢的表資料中的第一行,對該行資料中的<code>openid</code>字段所對應的值進行判斷,假如該值為123,則将這行資料加入到結果集當中,否則引擎接口繼續查詢下一行,如此不斷循環重複相同的判斷邏輯,直至對資料表中最後一行的資料判斷結束為止。最後,将結果集當中的所有滿足條件的資料回報給使用者。

以上便是執行器在查詢操作時候的大體功能,但是在進行更新操作(增、删、改)的時候,執行器還會将具體的操作記錄到binlog日志當中,另外,update會采取兩階段的送出方式,記錄到redolog中,也就是我們接下來所要講的MySQL日志系統。

日志日志,在生活中,顧名思義就是記錄生活的點點滴滴,把自己的内心世界記錄下來,更好的诠釋自己當時寫日志時候的心情感受。待到将來的某一天,我們回翻之前所寫的日志時,我們能夠瞬間回憶起自己身邊所發生的事以及内心世界。

同理,在計算機領域裡,日志也是一個相當重要的概念。而MySQL的日志系統會将我們對資料庫的修改操作全部自動記錄下來,這樣就能夠通過日志檔案随時對誤操作的資料進行恢複。說道這裡,有了解過Git或是讀過Taoye之前寫的Git文章的朋友,應該會有點聯想,在Git中,我們可以在任意時間點對項目的版本進行恢複(回退),雖然結果與MySQL的日志系統有點類似,但是它們的實作原理是不一樣的。關于Git,想了解的讀者可以暫且跳轉學習:哪些年,我們玩過的Git

《大話資料庫》-SQL語句執行時,底層究竟做了什麼小動作?

redo log(重做日志)和binlog(歸檔日志)是MySQL當中非常重要的兩種日志,尤其是binlog,在我們之後進行資料恢複或是搭建主從的時候會頻繁的接觸到。

關于對redo log和binlog的了解,丁奇在《MySQL實戰45講》中已經講解的非常明白了,其中以一種通俗易懂的方式來介紹了晦澀難懂的redo log和binlog。說實話,我記得第一次學習這兩種日志的時候,總是對某些小的知識點細節了解的不夠透徹,甚至會出現了解偏差的情況,好在後來通過不斷重複閱讀和查詢資料的形式一個個解決了當時的疑問。關于對redo log和binlog基本概念的了解,這裡就不再多說了,大家可以去看看《MySQL實戰45講》中該部分的内容,或是閱讀:https://www.cnblogs.com/sunshineliulu/p/10905483.html

下面主要是介紹下binlog二進制日志的常見操作,及如何通過binlog搭建主從和進行資料的恢複:

在MySQL中,binlog日志預設是處于關閉的狀态,我們要想開啟并設定二進制日志,就需要修改MySQL的配置檔案,即<code>my.cnf</code>,配置完成之後再重新開機mysql即可生效<code>systemctl restart mysql</code>:

log-bin:binlog日志的存放路徑

expire_logs_days:表示日志檔案的過期時間,過期之後MySQL就會自動将日志檔案删除,如果設定為0則表示不删除

binlog_format:表示的是binlog日志檔案的存儲格式。

該屬性的值總共有三種:statement、row和mixed,當将日志格式配置為statement的時候,表示每一次修改資料的SQL語句本身都會記錄到binlog日志當中,而row并非記錄SQL語句,而是記錄被修改的那行資料。mixed格式則是前兩總形式的混合,一般對庫或表結構發生修改采用的是statement,而資料本身發生修改采用row格式。

max_binlog_size: 每個binlog日志檔案的大小,預設是1G。因為binlog日志并不是像redo log那樣循環寫,而是采用追加的形式記錄日志,是以當一個binlog日志檔案寫滿之後,就會進行一次滾動,也就是建立一個新的binlog日志檔案用于記錄。

以上關于binlog日志的配置隻是一部分,其他的配置我們用到的時候再來補充。當該部配置設定置完成之後,我們就可以通過<code>show variables like</code>指令來檢視二進制檔案的設定:

檢視目前所存在的所有二進制日志,以及目前正在使用的二進制日志:

每當我們重新開機MySQL服務的時候,都會自動建立一個新的binglog二進制日志檔案,并且還會生成mysql-bin.index檔案,該檔案存儲所有二進制日志檔案的清單,也就是二進制日志檔案的索引。在上面,我們配置在<code>my.cnf</code>中配置binlog的時候,設定了expire_logs_days=7,也就是說超過7天的日志檔案會被自動删除。但是如果我們不對binlog日志檔案進行處理的時候,大量的binlog檔案會占據太多的磁盤空間,進而在一定程度上影響了磁盤的IO性能,是以定期清理binlog日志是很有必要的。

我們既可以清除所有的binlog日志檔案,也可以對指定的日志檔案進行清除,部分具體操作如下:

我們在執行<code>reset master</code>指令的時候,會清除所有的binlog日志,并且會自動重新建立新的二進制日志檔案,其其編号為000001開始。

此外,也可以通過<code>flush logs</code>指令實作日志檔案的滾動,即會生成的一個新的日志檔案作為目前的記錄日志,之後對資料庫的修改操作都會記錄在該日志檔案當中:

前面有提到,binlog中有statement、row和mixed三種存儲格式,它們的存儲内容都是二進制的形式,我們是無法通過正常的方式浏覽其内容的,一般可以以指令的形式或是工具來檢視并分析二進制日志檔案的内容:

除了采用<code>show binlog events</code>指令之外,我們還可以使用<code>mysqlbinlog</code>來讀取日志檔案或轉化為自己想要輸出的檔案形式,這樣便于在誤操作的情況下對日志檔案進行分析。這裡需要注意的一點是,在使用<code>mysqlbinlog</code>工具的時候,一般要進入到其所在目錄,預設存在于MySQL的bin目錄當中,并且在指定具體解析的日志檔案時,同樣要定位檔案所在位置。

下面我們從0開始建庫、建表,并實作資料的增删改操作,來詳細分析一下日志檔案中所記錄的資訊:

日志滾動、建庫db_test、建表tb_test,對資料表執行增删改操作

檢視日志檔案、将日志檔案輸出為log、sql檔案,以便之後的分析

日志分析

下圖是Taoye使用Sublime text打開<code>my-log.sql</code>檔案時所展現的内容(部分内容略過),從圖中可以看到,binlog日志當中已經記錄了我們對資料庫所做出的所有操作,包括建庫、建表、增删改,并且該日志檔案是存儲在磁盤當中,換句話說,及時我們的MySQL服務當機了,但是重新開機之後依然可以根據該檔案進行資料的恢複。

在上面,已經介紹了在實際使用過程中binlog日志的常見操作,其中包括binlog日志的配置、檢視、滾動、清除和分析等,接下來就是利用binlog日志實作資料的恢複了。

在一個伸手不見五指的黑夜,Taoye的同僚Yetao坐在辦公桌前,左手捧着一杯Java(咖啡),右手提着一塊闆磚。白天不斷隐忍了産品經理的折磨之後,此時的他心情非常的憤懑,最終它做出了一個明智的選擇:“删庫,跑路!”

第二天,産品經理發現資料庫已然被Yetao這臭小子清空了,于是把心中的那把火全部灑在Taoye身上,并且指令Taoye在一天之内恢複資料庫中所有的資料,否則滾蛋。

Shit,Yetao這臭小子居然做了這麼愚蠢的騷操作,居然還讓我背鍋、擦屁股。好在我的binlog日志玩的賊溜,否則還真的得滾蛋。下面我們來看看在實際的過程中,假如出現了資料的誤操作,我們該如何利用binlog二進制日志檔案對資料進行恢複?

基于單個binlog實作資料的恢複

首先,我們需要通過以上一些SQL語句來自定一個場景,以便我們實作資料的恢複:<code>flush logs;</code>進行日志檔案滾動,使得之後對資料庫的操作都能記錄在一個新的binlog日志檔案當中。之後建立db_test資料庫以及tb_test資料表,并在表中的插入兩條資料,最後删除id=1所對應的資料。

假如我們最後删除的那一條資料是一個誤操作,我們應當如何通過binlog日志檔案來恢複該條資料呢?

1.檢視以上所有操作被記錄在哪一個binlog當中,也就是目前所使用的binlog日志

2.由<code>show master status</code>指令我們可以知道,目前使用的是<code>mysql-bin.000002</code>日志,并且以上所有操作都已經被記錄在該日志檔案當中。對此,我們可以繼續使用<code>show binlog events</code>指令來指定該日志檔案,檢視一下該檔案所發生的事件:

通過執行以上指令,我們可以得到日志檔案所記錄每個事件的資訊,其中包括日志檔案名、起始位置、事件類型、服務id、終止位置、描述資訊,其中比較重要的是log_name、pos、end_log_pos、info字段的資訊,我們進行資料的恢複也是的根據這些資訊來實作的。

3.分析及資料的恢複

既然我們是想對最後的<code>delete</code>語句所删除的資料進行恢複,那麼根據上面的執行結果可以發現,倒數第二行中的Info字段的值為:<code>use 'db_test'; delete from tb_test where id=1</code>,也就是說我們的需求是想要得到執行該行之前的資料庫狀态。

對此,我們繼續往上查找,發現倒數第四行的Info是一個<code>commit</code>,也就是<code>insert</code>事務的送出。至此,我們基本可以确定,我們隻需要将資料庫恢複到從起始位置到該<code>commit</code>為止即可,這樣就完美的跳過了<code>delete</code>操作。而指定資料恢複的位置便是pos和end_log_pos兩個字段所決定的,從圖中我們就能明白:隻需要執行binlog日志檔案中pos=4,end_log_pos=848的内容即可恢複。

是以我們可以通過如下方式實作該資料的恢複:

以上是我們通過mysqlbinlog工具實作資料恢複的一般步驟,在其中指定的了--start-position和stop-position兩個參數來确定恢複的起始位置和終止位置。當然,在前面我們也有介紹過将binlog日志轉化為sql檔案方法,是以我們同樣可以基于sql檔案來實作資料的恢複,這個比較簡單,這裡就不多說了。

基于多個binlog實作資料的恢複

以上是我們基于單個binlog日志檔案實作資料恢複的操作,但是假如我們的資料操作資訊被記錄到多個binlog日志檔案當中,在誤操作的情況下應該如何實作日志的恢複呢?

在上面我們執行了三次<code>flush logs</code>指令,也就是滾動了三次,并生成了三個日志binlog日志檔案。第一個檔案中記錄了三次<code>insert</code>操作,第二個日志檔案中記錄了<code>insert、delete、insert</code>,第三個日志記錄了<code>insert、删表、删庫</code>的操作。假如我們現在的需求是恢複所有delete以及删表删庫的操作,我們應該怎麼做?

有了前一個例子的鋪墊,該資料的恢複應該不難,思路就是:第一個日志檔案進行全部恢複,第二個日志檔案進行兩次部分恢複(以delete為分界)、第三日志檔案進行一次部分恢複(即除去删表、删庫操作)。

具體的指令操作,大家可以參考删一個例子,基本是一緻的,這裡就不做過多贅述了。

參考資料:

[1] 丁奇.MySQL實戰45講

[2] 詳細分析MySQL事務日志(redo log和undo log)

https://www.cnblogs.com/f-ck-need-u/archive/2018/05/08/9010872.html#auto_id_11

[3] MySQL二進制日志詳解

https://www.cnblogs.com/jkin/p/10124182.html

至此,本文基本就結束了。

該文詳細講解了MySQL的體系結構,其中包括連接配接器、緩存、分析器、優化器和執行器等各個功能部件的作用及所扮演的角色。再者,介紹了MySQL的日志系統,日志系統在MySQL當中是非常重要的,其應用場景主要展現在主從搭建以及資料恢複,在實際生産過程中使用的還是非常頻繁的,畢竟誰也保證不了我們的資料庫沒有出現問題的時候。

這篇文章是《大話資料庫》的第一篇,也是從宏觀的角度來對MySQL進行一個整體性的分析,這樣對後面的事務、索引等知識的了解都是非常有幫助的,我們隻有了解了MySQL的底層工作原理,才能在出現問題時直擊問題的本質。

認真讀到這裡的讀者,相信都是和Taoye一樣懷揣着一顆想要不斷提高自己的心,想要成為一位優秀的Coder。原創不易啊,如果本文對各位有所幫助,還請關注+轉發+再看,【三連】走一走啊,就當是給Taoye堅持肝文的鼓勵和支援了。

《大話資料庫》,我們下期再見!