前言:我個人認為,有關MYSQL存儲過程/函數在MYSQL中的實現比較粗糙,可擴展性不夠好,其實現的耦合性太高,所以主要講一些它的原理方面的內容,但有可能在某些方面理解不夠好或者有些不正確的地方,歡迎指正,謝謝!
2012-5-14 by whuai QQ:329570985 歡迎指正!
在MYSQL中,同樣有很多類型的系統對象,包括表、視圖、存儲過程、存儲函數等,但由于MYSQL的插件式存儲引擎及其它實現方面的特點,其每一種對象的緩存方式都不同,或者說這些對象的緩存不是通過一種統一的方式來管理的,每一種對象的緩存都是有自己的特點,并且緩存的內容也有很大的差異,下面再敘述一下存儲過程(PLSQL)緩存方式。
MYSQL數據庫管理系統中的存儲過程/函數也是有緩存機制的,存儲過程/函數實際上是用戶通過創建存儲過程的語句創建好的系統對象,它具有指定的名字、類型(存儲過程/函數)及要執行的語句序列等。例如下面就是一個創建過程的語句:
create procedure p()
begin declare a int default 100;
declare b int default 1000;
declare d int default 1000;
begin
declare c varchar(100) default 'hello world';
insert into my values(a, c);
end;
end;
上面創建的過程名字為p,下面定義了一些變量并且都賦了初始值,一對BEGIN及END標志了一個語句塊的內容,語句塊可以嵌套定義,比如上面就在第一對BEGIN及END之間又定義了一對BEGIN及END,每一個語句塊中又可以單獨定義自己的變量,同時這些變量又有自己的可見性范圍,假設在內層語句塊中定義了一個變量,在外層同時又定義了一個同名的變量,那么在內層引用這個變量時實際上是內層定義的變量,而這個變量在外層是不可見的,可以引用到的只能是外層定義的變量。
在實現上(基于源代碼的分析),一個存儲過程/函數分析后會得到一個sp_head結構體對象,這個對象唯一對應一個存儲過程,而每一個語句塊對應一個sp_pcontext結構對象,這個對象之間存在著父子關系,一個父親可以有多個孩子,一個孩子只能有一個父親,比如上面例子中的存儲過程語句,整體的存儲過程P就對應一個sp_head結構體對象,第一個BEGIN對應的語句塊是父sp_pcontext,而其中又包括了一個語句塊,這個語句塊是父語句塊的一個子語句塊,在分析之后同樣會生成一個sp_pcontext對象,它是一個子語句塊對象,sp_pcontext結構體內有一個成員m_parent,它會指向父語句塊,實際上這個語句塊可以被稱為一個“上下文環境”,因為它是可以被看作像C語言中的一個語句塊,比如用{}括起來的一段代碼一樣。
在進行語法分析sp_compile時,MYSQL會對每一條分析的語句都生成相應的指令,這些指令都被順序存儲到類型為DYNAMIC_ARRAY的動態數組m_instr中,這是用來存儲所有的這個存儲過程的指令的,比如對于上面語句“declare b int default 1000;”,系統首先會分配一個變量的存儲空間,變量被放在sp_pcontext對象中,也是通過一個動態數組m_vars來存儲的,因為變量是語句塊級的,而上面這條語句還對應的一個操作就是給這個變量賦初始值,所以系統要創建一個指令給這個變量設置初值,這個指令為sp_instr_set,每一個指令都需要實現一個執行函數exec_core,這個函數是一個虛函數,每一種指令的執行都要實現自己的執行函數,就比如上面這個設置變量的指令,它的實現是調用了函數set_variable來給指定的變量設置指定的初始值即可。
對于不同的操作,有不同的指令,MYSQL包括的指令有:sp_instr_stmt(執行SQL語句的指令)、sp_instr_set(設置變量的指令)、sp_instr_set_trigger_field(設置觸發器中涉及到NEW/OLD變量的值的指令)、sp_instr_jump、sp_instr_jump_if_not(執行跳轉指令)、sp_instr_freturn(函數返回指令)、sp_instr_cpush(游標聲明指令)、sp_instr_copen(打開游標的指令)、sp_instr_cclose(關閉游標的指令)、sp_instr_cfetch(從游標取數據的指令)等,在PLSQL中涉及到這些操作后,都會創建相應的指令,并加入到sp_head的指令動態數組中,執行時會通過順序或者跳轉的方式執行。
在PLSQL中,本人最感興趣的是變量的引用,包括本地變量及上層語句塊的變量的引用,系統是如何正確的找到相應的變量的?或者是通過什么方式來找到的?其實sp_head中的每一個變量都對應一個編號,是按照分析順序生成的。變量是在sp_pcontext中定義的,也就是說變量的存儲單元是語句塊(sp_pcontext),一個語句塊中可以有多個變量。同時在每一個語句塊結構體sp_pcontext中都有一個表示這個語句塊中所定義的變量的編號的范圍,一個起始ID及變量個數,因為sp_pcontext是按照父子關系來聯系的,那么一個語句塊的開始變量ID號是其前面平行的語句塊的開始ID號加1的值,如果它本身就是第一個語句塊,則其起始語句塊的變量ID號為其父語句塊的結束ID號加1的值。所以這樣就給每一個語句塊指定了唯一的互不包含的變量ID號的范圍。
那么要引用一個變量時,找到其在符號表中的對象是很容易的,因為對每一個指令而言,都有一個指針指向其所屬的sp_pcontext,同時每一個引用變量操作對應的指令都記錄了這個變量的ID號,這樣系統可以直接根據sp_pcontext中的超始ID號及變量的個數計算出當前這個被引用的變量對應的ID號是否在當前語句塊sp_pcontext中,如果是則直接從sp_pcontext的變量動態數組m_vars中找到對應ID的變量對象,如果沒有找到,則說明這個變量有可能是在父語句塊中定義的,則通過sp_pcontext中的m_parent找到其父語句塊,用同樣的方法找對應的變量,如果找到則已,找不到繼續向上,依此類推,直到找到在某一個語句塊中的這個變量,或者m_parent為空的時候則說明沒有找到,則說明這個引用是一個對未定義的變量的引用,直接報錯即可。那么通過上面的方法只要找到這個變量對象,則對其訪問或者給它賦值,都可以直接訪問其成員函數即可。
由于PLSQL的數據類型及支持語句比較多,這里只介紹一些比較重要的原理,從上面所敘述的內容可以對PLSQL的分析、指令的生成及運行原理有一個大概的輪廓,從總的結構來講,存儲過程/函數生成的計劃就是一個sp_head對象,sp_head中包含了所有生成的指令,在運行過程中按照指令順序或者內部邏輯的跳轉來執行。另外生成一個語句塊的樹形結構,每一個樹節點為sp_pcontext結構對象,其中m_parent指向其父節點,同時每一個sp_pcontext還存儲了所有的子語句塊鏈表,PLSQL中定義的變量都存儲在sp_pcontext中。
本文還要講另外一個內容就是存儲過程/函數對象的緩存機制,其實在MYSQL中,緩存的并不是存儲過程/函數的字典定義的對象,也就是說不是像之前講的表對象的字典緩存,而是將整個分析好的sp_head對象緩存起來了。那么說白了,MYSQL的存儲過程/函數字典的緩存其實是其執行計劃的緩存。只要執行過一次,那么只要沒有將這個存儲過程/函數刪除,再次執行時只需要從緩存空間中找到這個計劃拿出來直接執行即可,這樣就提高了存儲過程/函數的執行效率,不需要再進行詞法、語法、語義、指令的生成等這些步驟了。
總結:在MYSQL中的存儲過程/函數的分析過程將上面提到的所有步驟都揉合到了一起,也就是說:詞法分析、語法分析、語義分析、指令的生成這些步驟的分析過程沒有一個階段性的區分,沒有明顯的區分各個階段的工作,而是將所有這些步驟都一起完成,每分析一條語句,詞法、語法做完之后,直接分析這條語句中的語義、判斷定義的變量是否存在、確定變量在語句塊中的位置,合法之后直接創建變量的空間,同時還要分配一個指令,為這個變量設置初始值等操作,所有這些直接在語法文件中完成了,在實現上難免非常混亂。