摘要:用戶在使用數據庫過程中,受限於內置函數的功能,部分業務不易實現時,可以使用自定義C函數實現特殊功能。本文通過兩個示例展示自定義C函數的實現過程。
前言
用戶在使用數據庫過程中,常常受限於內置函數的功能,部分業務不易實現,或實現后性能較差,在這些場景出現時可以考慮使用C編寫自定義函數來實現獨立功能。
例如用戶針對某些數據列需要使用C編寫的特定算法進行計算,如果放在業務層所性能不能接受,就可以嘗試有自定義C函數在實現功能的前提下保證實現效率。
粗略的來說,用戶使用C編寫的自定義函數會被被編譯成動態庫並且由數據庫在需要的時候載入。在一個會話中第一次調用一個特定的用戶定義函數時,數據庫進程會把動態庫文件載入到內存中以便該函數被調用。從這個角度來說,注冊自定義函數時需要准備編譯好的動態庫文件和函數定義,以下會以實際操作舉例。
數據類型
首先需要先明確,數據庫中支持的數據類型在使用自定義C函數操作時,必須將數據庫中的數據類型轉換為數據庫內核可以處理的相關類型,實際上數據庫內核在處理內置函數輸入時也是類似的操作:
例如比較常見的
常見的幾種類型中,需要注意的是,對text類的函數,C函數在實際處理時往往不是直接使用 gaussdb 內部的 text\* 類型進行處理的,而是使用C語言的標准類型 char\* 進行處理的。
這種情況下就可以先讀取到入參的地址,再通過gauss提供的內置C函數(比如簡單常用的TextDatumGetCString)轉換為 char\* 類型字符串進行操作。
應用實例
下面以兩個簡單的HelloWorld例子來說明自定義C函數創建的整體流程。
1. 最大公約數
最大公約數的計算比較簡單,但內部必然會涉及循環,使用SQL實現只能通過類似PL/pgSQL自定義函數或存儲過程的方法,如果要達到極限性能的話可以嘗試使用自定義C函數實現。
如果以正常的C/C++實現,我們可能會這樣來編寫代碼
int gcd_c(int a, int b){ int c = a%b; while(c) { a = b; b = c; c = a%b; } return b; }
如果轉換成gauss可用的C函數需要進行改造,例如改造成如下文件,並命名未gcd.cpp:
//postgres.h和fmgr.h為gauss中C函數固定宏和基本定義的頭文件 #include "postgres.h" #include "fmgr.h" //PG_MODULE_MAGIC為固定調用宏,自定義c函數必須在文件開始位置包含 PG_MODULE_MAGIC; //下面兩行也是固定調用,這兩行可以指定出一個對外開放,並可在自定義C函數創建時被引用的函數 //其中gcd為動態庫對外可見接口,后面定義C函數時會用到 extern "C" Datum gcd(PG_FUNCTION_ARGS); PG_FUNCTION_INFO_V1(gcd); //實際處理函數 int gcd_c(int a, int b){ int c = a%b; while(c) { a = b; b = c; c = a%b; } return b; } //主入口函數 //PG_FUNCTION_ARGS是一個固定宏,實際是一個入參出參相關信息的結構體 Datum gcd(PG_FUNCTION_ARGS) { //入參階段 //檢查 參數是否為空,如果為空,返回空,相當於對空做檢查,防止創建函數時未指定strict屬性,導致函數執行異常 if(PG_ARGISNULL(0) || PG_ARGISNULL(1)){ PG_RETURN_NULL(); } //PG_GETARG_INT32(n)表示從PG_FUNCTION_ARGS結構體中抽取入參的第n個參數,並返回為int32類型 int32 arg1 = PG_GETARG_INT32(0); int32 arg2 = PG_GETARG_INT32(1); //調用實際執行函數 int32 res = gcd_c(arg1, arg2); //返回階段 PG_RETURN_INT32(res); }
編譯動態庫,編譯時請保證gcc/g++>=5.4
g++ -c -fpic -Wno-write-strings -fstack-protector-all gcd.cpp -I ${GAUSSHOME}/include/postgresql/server/cfunction
g++ -shared -fPIC -Wl,-z,now -o gcd.so gcd.o
執行創建C函數命令:
create or replace function gcd_my(integer,integer) returns integer as 'xxxxx/gcd.so', 'gcd' language c strict not fenced immutable shippable;
這個命令中
- gcd_my表示后面sql中調用該C函數時使用的名字
- xxxxxx/gcd.so表示的是當前環境上編譯生成動態庫放置的位置
- language c表示該自定義函數為C語言編寫的函數
- not fenced表示該函數執行時使用的是非fenced模式(該模式后面小節會再次說明)
- strict表示輸入不能為空,否則返回也為空(上部分C函數雖然以對空值做了處理,當前為了說明問題,此處也聲明了strict屬性)
- 其余部分為create function的公共內容,具體可以參考手冊中create function章節
執行函數
select gcd_my(12, 16);
2. 將字符串中的第一個字母大寫,其他小寫
內置的字符串處理函數中有大小寫轉換函數,但沒有這種類似只有第一個字符進行大小寫轉換的定制化函數,如此,我們就可以嘗試使用自定義C函數進行實現。
如果以正常的C/C++語言操作,我們可能會這樣來寫
#include <string.h> void upper_str(char* str){ bool has_first = false; for(int i = 0; i< strlen(str); i++){ if((str[i]>='a' && str[i]<='z') || (str[i]>='a' && str[i]<='z')) { if(has_first) { str[i]=str[i]%0x20 + 'a'; } else { str[i]=str[i]%0x20 + 'A'; } } } }
如果轉換成gauss可用的C函數可以改造成如下文件,並命名未upper_str.cpp:
#include "postgres.h" #include "fmgr.h" //builtins.h中存在下面使用的TextDatumGetCString的定義 #include "utils/builtins.h" #include <string.h> PG_MODULE_MAGIC; extern "C" Datum upper_str(PG_FUNCTION_ARGS); PG_FUNCTION_INFO_V1(upper_str); //實際處理函數 void upper_str_c(char* str){ bool has_first = false; for(int i = 0; i< strlen(str); i++){ if((str[i]>='A' && str[i]<='Z') || (str[i]>='a' && str[i]<='z')) { if(has_first) { str[i]=str[i]%0x20 + 'a' - 1; } else { str[i]=str[i]%0x20 + 'A' - 1; has_first=true; } } } } //主入口函數 Datum upper_str(PG_FUNCTION_ARGS) { //入參階段 if(PG_ARGISNULL(0)){ PG_RETURN_NULL(); } Datum source = PG_GETARG_DATUM(0); char *src = TextDatumGetCString(source); //實際調用函數 (void) upper_str_c(src); //返回階段 //cstring_to_text可以將char*類型轉換為gauss內置的text*類型 //PG_RETURN_TEXT_P宏可以返回text*類型的結構,被上層調用獲取 PG_RETURN_TEXT_P(cstring_to_text(src)); }
執行創建C函數命令:
create or replace function upper_str_my(text) returns text as 'xxxxxx/upper_str.so', 'upper_str' language c strict not fenced immutable shippable;
執行函數
select upper_str_my('@hello World');
注意事項
1. fenced/not fenced 模式
C函數在注冊時有一個選項時fenced,這個模式是gauss提供的一種進程隔離機制。如果在fenced模式下實際執行的函數會放在一個單獨啟動的進程中執行,而not fenced模式則是在實際執行時和gaussdb同一進程。兩種模式各有優劣,需要根據實際情況進行選擇。比較建議的方式是,受限創建為fenced模式函數,調試無問題后,再重新注冊為not fenced模式,以高效率模式運行。
- * fenced模式
優點:執行更安全,如果函數執行出現異常,不會影響到CN/DN進程,保證整個節點的穩定運行;
缺點:需要額外的進程開銷,效率較低。 - * not fenced模式正好相反
優點:運行效率高,無額外開銷;
缺點:如果自定義C代碼編碼有問題,容易造成CN/DN進程異常等嚴重問題。
2. C函數編寫時,需要注意注冊時的參數類型和返回類型,系統會根據創建函數時指定的內容傳給實際執行的函數,所以如果函數內部處理和創建時指定類型不一致容易出現不可預測的異常
3. C函數編寫時,需要注意對空值進行特殊處理,或者再創建函數時指定為strict屬性的函數
4. C函數實現時都是底層實現,應該嚴格控制不可靠C函數的創建,堅持慎重使用自定義C函數的原則
5. C函數創建只能由具有sysadmin權限的用戶進行創建,可以通過grant操作賦予其他用戶執行權限
總結
自定義C函數的存在給用戶提供了直接實現底層邏輯的機會,一個實現完善的自定義C函數,往往可以給業務帶來極強的定制性和大幅度的性能提升。但定制性也是一把雙刃劍,如果編寫自定義C函數時做的不夠完善存在邏輯問題,甚至內存溢出等嚴重問題,也極可能使系統崩潰。因此可以使用自定義C函數作為一個強有力的工具為實際業務增光添彩,但也需要謹慎的創建使用任意一個C函數避免誤傷其他業務。
本文分享自華為雲社區《初窺自定義C函數》,原文作者:sincatter 。