在此前發布的(de)(de) TDengine 3.0.4.0 版本中(zhong),我們(men)引(yin)入的(de)(de)一個重磅新(xin)功(gong)(gong)能是“使用(yong) Python 語(yu)言(yan)編寫用(yong)戶(hu)自(zi)定(ding)義(yi)函數(shu)(UDF)”。這一突破性功(gong)(gong)能不僅為數(shu)據庫(ku)操作提供了前所未有(you)的(de)(de)靈活性,而(er)且極大地降低了技術門檻——即使是編程(cheng)初學者也能使用(yong) Python 語(yu)言(yan)輕松地定(ding)制和操縱數(shu)據庫(ku)。這些自(zi)定(ding)義(yi)函數(shu)可以像內置函數(shu)一樣在 SQL 語(yu)句中(zhong)靈活使用(yong),實現個性化數(shu)據處理。本篇文章中(zhong)將詳細介紹如何有(you)效(xiao)利用(yong)這一功(gong)(gong)能,開啟你的(de)(de)數(shu)據庫(ku)自(zi)定(ding)義(yi)之旅。
UDF 基礎操作
創建 UDF
我們先來(lai)看下如(ru)何創建 Python 的 UDF 函(han)數:
CREATE [OR REPLACE] [AGGREGATE] FUNCTION function_name
as library_path OUTPUTTYPE output_type [BUFSIZE buffer_size] [LANGUAGE 'C‘ | ’Python']
選項說明:
1) CREATE [OR REPLACE]:第一次全新創(chuang)建使用 CREATE ,已經創(chuang)建要更(geng)新代碼可以加上(shang) REPLACE
2)AGGREGATE:可選項,加上此選項表示(shi)創建的是(shi)聚合函(han)數,不加是(shi)投影函(han)數
3)function_name:自定義(yi)函數(shu)名(ming)稱,創建完后在 SQL 語句中使(shi)用(yong)的名(ming)稱,最大為 64 字節,超出部(bu)分會截斷
4)OUTPUTTYPE:自定義函數(shu)(shu)輸出數(shu)(shu)據類型,支持類型如下:

5)BUFSIZE:設置自(zi)定義函數(shu)可(ke)以使(shi)(shi)用(yong)(yong)的(de)內存緩存大(da)小 ,此選項僅在使(shi)(shi)用(yong)(yong) AGGREGATE 時才(cai)有效,也就是(shi)說只有創(chuang)建聚合(he)函數(shu)才(cai)能使(shi)(shi)用(yong)(yong)此選項。最大(da)為 256k,計(ji)數(shu)單位為 byte。分配的(de)緩存是(shi)留(liu)給 Python 自(zi)定義函數(shu)使(shi)(shi)用(yong)(yong)的(de),緩存生命(ming)周期是(shi)從調用(yong)(yong)聚合(he)函數(shu) start 開始(shi)到調用(yong)(yong) finish() 結束(shu)的(de)整個聚合(he)計(ji)算過程,所以可(ke)以當做全局變量(liang)來使(shi)(shi)用(yong)(yong)。
6)LANGUAGE 是創建自定義(yi)函數的(de)語言,目(mu)前 TDengine 支持 Python 和 C 兩種(zhong)。
刪除 UDF
DROP FUNCTION function_name; 是(shi)刪除指定名稱(cheng)的 UDF 函數(shu)。function_name 參數(shu)的含義與 CREATE 指令中(zhong)的 function_name 參數(shu)一致,即要刪除函數(shu)的名稱(cheng)。
查看 UDF
使用 SHOW FUNCTIONS; 可以(yi)簡(jian)單查(cha)看(kan)下(xia) UDF 創建(jian)的函數名。使用 select * from information_schema.ins_functions; 可以(yi)詳(xiang)細查(cha)看(kan) UDF 創建(jian)時(shi)候(hou)的各個參數,加(jia) \G 可以(yi)以(yi)豎列(lie)查(cha)看(kan)到完整內容,如(ru):select * from information_schema.ins_functions\G;
安裝環境
接下來帶你(ni)安裝(zhuang) Python UDF 的開(kai)發環境,安裝(zhuang)過程比較簡單。
首先,準備安裝環境
1) CMAKE:最(zui)低版本要(yao)求 3.0.2
2) GCC:因為需要(yao)在(zai)本(ben)機編(bian)譯支持 Python UDF 函數的 so 文(wen)件,所以(yi)需要(yao)安裝 GCC 環(huan)境,GCC 版(ban)本(ben)最低要(yao)求(qiu) 7.5 以(yi)上(shang)。
3)Python:要求 3.7 及以(yi)上(shang)版(ban)本
然后(hou)開(kai)始安裝插件 Python3 -m pip install taospyudf,安裝成功后(hou),執行 ldconfig。這樣(yang),開(kai)發環(huan)境(jing)就(jiu)已經安裝就(jiu)緒了(le)。
編寫 UDF 函數
按上面步驟環境準備(bei)好(hao)后,我們便可以(yi)開始編寫自己的(de) UDF 函數(shu)了。UDF 函數(shu)分兩(liang)大(da)類(lei)(lei),一類(lei)(lei)是(shi)投(tou)影(ying)函數(shu),另(ling)一類(lei)(lei)是(shi)聚合函數(shu)。這兩(liang)類(lei)(lei)函數(shu)創建及編寫都是(shi)完全不相同的(de),所(suo)以(yi)這里分開介紹。
在(zai)介紹函(han)數前,我們先(xian)來了(le)解下(xia)(xia)自定義函(han)數的(de)調用流程,如下(xia)(xia)圖:

首先 UDF 處理(li)框架(jia)是(shi)(shi)以數(shu)(shu)據(ju)(ju)塊(kuai)為處理(li)單位,每(mei)次調用到自(zi)定義(yi)函(han)數(shu)(shu)中時(shi)輸入數(shu)(shu)據(ju)(ju)都是(shi)(shi)一(yi)個數(shu)(shu)據(ju)(ju)塊(kuai),通(tong)過調用數(shu)(shu)據(ju)(ju)塊(kuai)對(dui)象(xiang)的 data(row,col) 方(fang)法輸入行號和列號可以取到數(shu)(shu)據(ju)(ju)塊(kuai)中任何一(yi)個位置上的數(shu)(shu)據(ju)(ju)。這(zhe)樣做的目(mu)的是(shi)(shi)減(jian)少 C 框架(jia)與(yu) Python 語(yu)言(yan)之間的調用次數(shu)(shu),提(ti)升性能。
返(fan)回數(shu)據時,投影函數(shu)原(yuan)來有多少行(xing),就(jiu)需(xu)要(yao)返(fan)回相同的行(xing)數(shu),聚合函數(shu)只(zhi)需(xu)要(yao)返(fan)回一行(xing)即可(ke)。下面詳細(xi)進行(xing)介(jie)紹:
投影函數
投影函(han)數就像(xiang)它的名字(zi)一樣,像(xiang)是一個(ge)投影,輸出(chu)數據的行數與輸入數據的行數需(xu)保持相(xiang)同,如果不相(xiang)同會報錯。下(xia)面用一個(ge)完整的例子來說明,我們(men)來實現一個(ge) TDengine 內置(zhi)的 concat 字(zi)符串連接函(han)數,如下(xia):
1) 編寫函數

函數說明:
- init – 在 UDF 模塊初始化的時候調用一次,可以做一些初始化的工作
- destroy – 在 UDF 模塊退出的時候調用一次,可以做一些退出的工作
- process – 每個數據塊到來后要調用的數據處理函數,調用 shape() 方法返回數據塊行及列數
- nrows 返回數據塊擁有的行數
- ncols 返回數據塊擁有的列數,列數實際是 concat() 函數的參數個數
返回值:
- 投影函數的返回對象必須是一個列表,非列表對象會直接報錯
- 列表對象中元素的個數應該與塊行數 nrows 相同,否則也會報錯
2) 創建函數
編寫好自定義函數后,我們就可以直接在 taos – shell 中創建了,輸入如下:create function py_concat as '/home/py_concat.py' outputtype varchar(256) language 'Python';
創建函數名 py_concat ,創建 Python 文件位(wei)置在 /home/py_concat.py,輸出(chu)數據(ju)類型為 varchar,長度 256 字節,語言為 Python。
3)執行函數
上步操作成功后,即可像內置函數一樣在 SQL 中任意使用,如 taos-shell 中輸入 select sf_concat(factory_name,room_name), concat(factory_name,room_name) from devices;
grade_name class_name 均為 varchar 數據類型
把工(gong)廠和(he)(he)車間名(ming)連(lian)接成一個字符串返回,可以對比 UDF 輸出和(he)(he)內置函數輸出結果,預期是(shi)相同的。
聚合函數
聚合(he)函數是把數據進行聚合(he)計算,最后只(zhi)輸出(chu)一行聚合(he)結果(guo)即可。這里我們(men)用一個大家最熟(shu)悉(xi)的(de)統計個數 count 實(shi)例來(lai)講解:
1) 編寫函數

實現原理:
- 計數的累加值在 start 初始化回調的時候把 0 值保存進 buf 中做為初始值
- 在 reduct 函數中,如果不為 None 就不斷累加,在 reduce 返回值會被存儲在 buf 中,下次回調 reduce 時再作為參數 buf 傳過來,這樣可以反復使用 buf
- 最后在 finish 函數中,buf 也會通過參數傳進來,把自己前面存儲在 buf 中的值取出來,作為返回值返回,即為最終 count 結果。
函數說明:
- init – 在 UDF 模塊初始化的時候調用一次,可以做一些初始化的工作
- destroy – 在 UDF 模塊退出的時候調用一次,可以做一些退出的工作
- start. – 開始進行聚合函數計算時調用一次,主要完成對聚合函數使用的緩存進行初始化
- reduce – 每個數據塊到來后要調用的數據處理函數, 調用 shape() 方法返回數據塊行及列數
- rows 返回數據塊擁有的行數
- cols 返回數據塊擁有的列數,列數是傳入 UDF 函數的參數個數
此函數會被循環調用
- finish – 計算最終聚合結果,此函數只在最后調用一次,在此函數中返回最終結果
返回值:
返(fan)回的(de)數據類型(xing)為創建(jian) UDF 函數時指(zhi)定的(de) OUTPUTTYPE 數據類型(xing),返(fan)回類型(xing)不正(zheng)確會報錯,允許 None 對象(xiang)返(fan)回。
2) 創建函數
編寫好自定義函數后,我們就可以直接在 taos – shell 中創建了,輸入如下:create aggregate function af_count as ''/home/af_count.py'' outputtype bigint bufsize 4096 language 'Python';
創建函數(shu)名 af_count ,創建 Python 文件位置在 /home/af_count.py,輸出數(shu)據類(lei)型(xing)為(wei) bigint,語言為(wei) Python。
3)執行函數
上步操作成功后,即可像內置函數一樣在 SQL 中任意使用,如 taos-shell 中輸入select af_count(col1) from devices; 。這樣你就擁有了自己的(de) count 統(tong)計函數(shu),想要統(tong)計什么(me),完全由你自己來決定。
數據類型映射關系
Python 語言與 C 語言交互,最(zui)重要的(de)就是數據類(lei)型如何轉化的(de)問題。也是 Python UDF 函數編寫最(zui)容易出(chu)錯的(de)地方(fang),所以這里(li)要重點介紹下:
首先(xian)我們看(kan)下映射關系表:

1)int 類型在 Python3 中沒(mei)有大(da)小限制
2)binary / nchar / varchar 類(lei)型(xing)(xing)都(dou)映射為了 Python 的 bytes 對象,所(suo)以在使用的時候要加(jia)以區別(bie)(bie)——varchar 數據類(lei)型(xing)(xing)是(shi) binary 的別(bie)(bie)名。
因為 binary 和 nchar 都映射(she)為了(le)相同的(de)(de) bytes 數據類型,所(suo)以(yi)自(zi)定(ding)義函(han)數的(de)(de)開(kai)發者自(zi)己(ji)約定(ding)輸(shu)入自(zi)定(ding)義函(han)數參(can)數的(de)(de)類型,不(bu)同數據類型需要不(bu)同的(de)(de)轉化方式:
- binary 類型轉 str 對象, 使用 bytes.decode(‘utf-8’)
- nchar 類型轉 str 對象, 使用 bytes.decode(‘utf_32_le’)
當(dang)把 str 對象內容輸(shu)出為(wei) OUTPUTTYPE 指定(ding)的(de)不同類(lei)型時(shi),也需要進行區分:
- str 對象當 binary 類型返回,str.encode(‘utf-8’)
- str 對象當 nchar 類型返回,str.encode(‘utf_32_le’)
開發技巧
更新函數代碼
由于(yu)我們(men)在開發(fa) UDF 函數時需要(yao)頻繁修改代碼(ma)再調試運行,因此需要(yao)知(zhi)道如何讓新修改的(de)代碼(ma)生效。

從上(shang)(shang)面流(liu)程圖中(zhong)我(wo)們(men)可以(yi)看(kan)到,在創建 UDF 函數時指(zhi)定(ding)的 .py 文(wen)件(jian)路徑,只在創建的那(nei)一(yi)時刻使用(yong),把文(wen)件(jian)內(nei)容(rong)讀取出來存(cun)放到 mnode 中(zhong),便于在集群的任何節點上(shang)(shang)都可以(yi)使用(yong)。所以(yi) .py 文(wen)件(jian)創建完 UDF 后就不再使用(yong)了,我(wo)們(men)更(geng)新(xin)了 .py 文(wen)件(jian)中(zhong)的代碼后,需(xu)要再把 .py文(wen)件(jian)中(zhong)的代碼更(geng)新(xin)到 MNODE 中(zhong)才可以(yi)起作(zuo)用(yong)。
目前更(geng)新(xin) Python 自定義(yi)函數的代碼(ma)提供了直接更(geng)新(xin)的命令:

增加 OR REPLACE 即可(ke)直(zhi)接把 library_path 指向(xiang)的 Python 自定義函數的內(nei)容(rong)更新到 MNODE 中,再(zai)次調用就(jiu)會(hui)使(shi)用更新后(hou)的代碼了。
輸出日志
TDengine 的(de)(de) Python UDF 不支(zhi)持(chi) Python 代(dai)碼的(de)(de)調試,但支(zhi)持(chi)了日志輸(shu)出(chu),如大家(jia)常用(yong)的(de)(de) logging 庫,都(dou)可以在 UDF 函數中使用(yong),建議日志輸(shu)出(chu)到文件(jian)中查看。print 函數打(da)印的(de)(de)信息是(shi)看不到的(de)(de),所以就不要用(yong)此函數輸(shu)出(chu)信息了。如:

拋出異常
如果開(kai)發者在(zai) UDF 函數在(zai)檢測到異常(chang)(chang)數據(ju)后(hou),需(xu)要終止查(cha)詢,可以(yi)通過(guo) raise 方式拋出異常(chang)(chang),同時(shi)要保證自(zi)己(ji) raise 的(de)異常(chang)(chang)不會(hui)被自(zi)己(ji)捕獲,因(yin)為自(zi)己(ji)捕獲了框架(jia)就捕不到了,所以(yi)要位于(yu)最上層拋出異常(chang)(chang)。
拋出(chu)的異常會(hui)被(bei) UDF 框(kuang)架(jia)捕獲到并終止當前查詢,同時(shi)在應用端調(diao)用的查詢接口(kou)中會(hui)相應的返回專(zhuan)屬的 UDF 函數拋出(chu)異常錯(cuo)誤碼(ma):0x8000290D,應用程序(xu)可根據(ju)此錯(cuo)誤碼(ma)做應用層的相應處理。
拋出(chu)的錯誤可以(yi)在 taos.cfg 中配(pei)置的日(ri)志文件的目錄下找到(dao),異常日(ri)志輸出(chu)到(dao)了taosudfpy.log 中了,如(ru)下是一實(shi)例(li)輸出(chu)的異常日(ri)志:

異常日(ri)(ri)志(zhi)的(de)內容及發生異常的(de)文件(jian)(jian)名(ming)及行號都在(zai)此日(ri)(ri)志(zhi)文件(jian)(jian)中。
查看 UDF 框架生成的日志
UDF 框架返(fan)回(hui)錯(cuo)誤描(miao)述時只(zhi)按(an)大數返(fan)回(hui),所以詳(xiang)細(xi)的(de)錯(cuo)誤原因還需要(yao)開發者(zhe)通過(guo)日志(zhi)來查(cha)看。相關(guan)日志(zhi)主要(yao)有兩個,都在 TDengine 日志(zhi)目錄下(xia)存放,這里(li)分(fen)別(bie)介紹下(xia):
1)taospyudf.log(UDF for Python 的日志文件)
日志(zhi)文(wen)件中記(ji)(ji)錄的是進程 udfd 加載 Python UDF 函數,執(zhi)行 UDF 函數過程中發生(sheng)的異常、錯誤及調用過程等的記(ji)(ji)錄。我(wo)們開發 pythyon udf 函數主要看這(zhe)個日志(zhi)文(wen)件就可以(yi)了。
2)udfdlog.0
這個是(shi) udfd 進程的(de)框架日志(zhi),里面包括調用 C 及 Python 等多種語(yu)言(yan)的(de)UDF 函數(shu)在(zai)框架中出的(de)錯的(de)日志(zhi),都會記(ji)錄在(zai)此文件(jian)中,所以這個日志(zhi)文件(jian)如果整個大框架出問題了,日志(zhi)會記(ji)錄在(zai)這里,一般情況下不(bu)用看。
常見錯誤碼說明
以下錯誤(wu)碼及(ji)錯誤(wu)描述(shu)是在開發 UDF 函數時經(jing)常會遇到的,這里做下說明(ming):

其(qi)中 10 和 12 是開發 Python UDF 最容易遇(yu)到(dao)的(de)錯誤,詳細(xi)的(de)原(yuan)因需要查看日(ri)志 taospyudf.log 。
參考實例
在(zai) TDengine 的開源倉庫中有幾個 UDF for Python 的測試實例(li),可供大家參(can)考:
實例被如(ru)下 CI 測試(shi)用例使用:
運行測試用例:
1)正(zheng)確安(an)裝(zhuang) Python3
2) 拉 TDengine 社區版代碼下來
3)進(jin)入 TDengine/tests/system-test/ 目錄
4)運行 Python3 test.py -f others/udfpy_main.py
注意事項
UDF 投影函數最容易在返回行數上出錯
UDF 投影函數(shu)(shu)要(yao)求(qiu)返回(hui)行(xing)(xing)數(shu)(shu)與輸入(ru)行(xing)(xing)數(shu)(shu)相同,在處理時如果程序(xu)稍復(fu)雜(za)一(yi)些就容易(yi)犯此錯誤,開發過程中(zhong)一(yi)定要(yao)小(xiao)心,不(bu)要(yao)漏(lou)行(xing)(xing)了(le)。
OUTPUTTYPE 輸出類型不匹配錯
UDF 函數要求最終返回的數據類(lei)(lei)型(xing)一定(ding)要和創(chuang)建(jian)函數時指定(ding)的 OUTPUTTYPE 類(lei)(lei)型(xing)一致,如果不一致就(jiu)會(hui)報錯。所以這里(li)也(ye)是非常容易出(chu)錯的地方。
投影函數的返回值在 process 函數中(zhong),必須要(yao)求是(shi) list 對象,list對象中(zhong)放的一定都是(shi) OUTPUTTYP 映射匹配(pei)的類型(xing)對象。
聚合函(han)數(shu)的最后返(fan)(fan)回值在 finish 函(han)數(shu)中,聚合函(han)數(shu)中返(fan)(fan)回一個(ge)值,這(zhe)個(ge)值一定(ding)(ding)是創建函(han)數(shu)時指定(ding)(ding)的 OUTPUTTYPE 映射匹配的 Python 類(lei)型值。
一直報無法加載 libtaospyudf.so 文件
按照上面(mian)安(an)裝步驟(zou)已經(jing)安(an)裝了 taospyudf 插件,但還是報 libtaospyudf.so 無法加載的錯,如下:

這個庫一(yi)般會安裝(zhuang)在(zai) /usr/local/lib/ 目(mu)(mu)錄下(xia)(xia)及 Python 的(de)插件目(mu)(mu)錄下(xia)(xia),lib 目(mu)(mu)錄是正式(shi)使用的(de),插件目(mu)(mu)錄是安裝(zhuang)時(shi)產生的(de),或(huo)者使用 find / -name ‘‘ 查詢在(zai)本地的(de)具體(ti)位置(zhi)。
如果(guo)文件(jian)沒有(you)或不存(cun)在(zai),那(nei)可能插件(jian)安裝(zhuang)有(you)問題,需要重新卸載安裝(zhuang)插件(jian)如果(guo)文件(jian)存(cun)在(zai),還(huan)是(shi)(shi)報(bao)這個錯,那(nei)使用 ldd 查看 so 文件(jian)依賴的(de)Python 庫文件(jian)是(shi)(shi)否能夠找到(dao),如下是(shi)(shi)典型的(de) Python 安裝(zhuang)的(de)有(you)問題:

如上錯誤(wu),重(zhong)新正(zheng)確安裝(zhuang) Python3.9 可解決此問題。
結語
目前我們(men)推出的是 UDF for Python 的初(chu)版 1.0,該(gai)版本的某些功(gong)能(neng)仍在(zai)持續完(wan)善(shan)中(zhong)。我們(men)誠(cheng)邀大家積(ji)極(ji)使用這一(yi)功(gong)能(neng),定義(yi)完(wan)全(quan)屬(shu)于(yu)自己的數據庫。同(tong)時(shi),我們(men)也希(xi)望大家在(zai)使用過程(cheng)中(zhong)多提寶貴(gui)意見,以便(bian)我們(men)共同(tong)改進(jin)并增強這一(yi)功(gong)能(neng),讓(rang)這個能(neng)解決特殊需求的實用功(gong)能(neng)越來(lai)越便(bian)捷、強大。


























