T-SQL里数据库工程师都不知道的秘密之SQL Server自定义函数UDF
T-SQL SQL Server UDF自定義函數概念與案例實戰
函數的定義
這里的函數指的是用戶自定義函數(UDF)全名為(user-defined function),以下簡稱為函數。
它是數據庫里的用戶自定義程序,用戶可以指定輸入參數,制定計算邏輯,最終返回一個標量的值或者結果集。一般我們通過T-SQL或者CLR來定義函數,這里我們重點介紹的是T-SQL的方式。還有一類函數是系統內置的函數又稱之為系統函數,我們直接調用即可。
函數的使用場景
函數可以在很多場景下使用,對一個標量和表(表變量)的字段或者計算列、檢查約束里都可適用。
函數的語法限制
有些語法在函數是不支持的,詳細見下:
1 用戶錯誤異常處理,不能使用TRY CATCH、@ERROR、RAISERROR等語法
2 修改數據(DML),對表的數據進行增加、修改、刪除,表變量除外。
3 使用用戶自定義(DDL)即對表結構的增加、修改、刪除
4 不可以調用自定義存儲過程,但可以調用擴展存儲過程
5 不能用臨時表
6 不能用動態SQL
7 不能返回多個結果集
8 SET語句不能使用
9 不能調用系統影響型函數如NEWID、RAND
完整內容詳細見官網介紹:?自定義函數的限制
函數的返回類型
函數按照返回類型可分為標量值函數、內聯型函數、多語內聯型函數,下文將結合案例分別介紹這些類型的函數。
標量型函數
標量型函數即是指函數返回的值是個標量(單個值)。這種類型的函數在函數體的頭部需要指定返回的類型(如int、money、varchar等)。
#這里假設我們有個層次(父子)的樹形員工數據,比如某個職位是HR MANAGER(HR經理)的,他下面有一些職位是HR的同事,這些職位是HR的下面是職位是HR Intern(實習HR)的同事。
DROP TABLE IF EXISTS dbo.Emp; GO CREATE TABLE dbo.emp ( empid INT NOT NULL CONSTRAINT PK_emp_empid PRIMARY KEY, mgrid INT NULL CONSTRAINT FK_emp_empid REFERENCES dbo.emp, empname VARCHAR(25) NOT NULL, jobtitle VARCHAR(25) NOT NULL, salary int NOT NULL, CHECK (empid <> mgrid) );INSERT INTO dbo.emp(empid, mgrid, empname,jobtitle, salary) VALUES(1, NULL, '張三','CEO', 10000), (2, 1, '李四','CTO', 7000), (3, 1, '王五','COO', 7500), (4, 2, '劉二','Product MANAGER', 6000), (5, 2, '馬六','Program MANAGER', 5500), (6, 2, '秦一','Test MANAGER', 4500), (7, 3, '宋二','HR MANAGER', 5000), (8, 4, '江五','Product', 5000), (9, 5, '谷三','C++', 2500), (10, 5, '谷三','Python', 2500), (11, 5, '楊八','Java', 3000), (12, 6, '紀一','Test', 2500), (13, 6, '黎七','Test', 2500), (14, 7, '管一','HR', 3000), (15, 7, '關三','HR', 3000), (16, 14, '孔十','HR Intern' , 2000), (17, 15, '金三','HR Intern', 2000), (18, 15, '錢二','HR Intern', 1500); -- 傳一個參數(員工姓名)后獲取他(她)下面的員工個數。 -- # 這里假定員工姓名唯一,比較嚴謹的是通過員工編號作為參數。 CREATE OR ALTER FUNCTION dbo.fun_getEmpnumber(@empname AS VARCHAR(10)) RETURNS INT AS BEGINDECLARE @totalemp AS INT;WITH EmpsCTE AS(SELECT empid,mgrid, salary,empname,jobtitleFROM dbo.EmpWHERE empname = @empnameUNION ALLSELECT S.empid,S.mgrid, S.salary,S.empname,S.jobtitleFROM EmpsCTE AS MINNER JOIN dbo.Emp AS SON S.mgrid = M.empid)SELECT @totalemp= COUNT(empid) - 1 FROM EmpsCTE;RETURN @totalemp; END; -- 調用, -- 返回員工”宋二”下的員工數(不含自己)。 SELECT dbo.fun_getEmpnumber('宋二') 5 -- 返回員工”紀一”下的員工數(因下面沒人,所以結果為0)。 SELECT dbo.fun_getEmpnumber('紀一') 0 -- 返回員工”方四”下的員工數(因沒有”方四”這個人,所以結果為-1)。 SELECT dbo.fun_getEmpnumber('方四') -1 -- 當然也可以結合字段一起使用 SELECT empid,mgrid, salary,empname,jobtitle, dbo.fun_getEmpnumber(empname) undernum FROM dbo.emp WHERE empname='宋二'確定與非確定值函數
像SYSDATETIME、 RAND(不帶種子)、NEWID內置的系統函數都是不確定函數,所謂不確定函數即是每次執行時返回的結果不固定、不確定,而固定函數則是如果參數給定,那么函數的返回值必然確定。
這里對于不確定函數按照函數對SQL Server系統的影響又分為系統獨立型和系統影響型,SYSDATETIME屬于系統獨立型,因為每次返回的時間不固定,但對下一次的執行不受上一次的影響。而RAND和NEWID則是系統影響型,即下一次的執行受上一次的影響,因為這倆函數具有唯一性,每次執行出來的結果都依賴于上一次的結果且不能和它一樣。
所以SYSDATETIME可以在用戶自定義函數里引用,而RAND和NEWID則不可以。示例見下:
CREATE OR ALTER FUNCTION dbo.fun_rand() RETURNS FLOAT AS BEGINRETURN RAND(); END; GO報錯信息見下:
如果想繞過這個限制,可以將不確定值函數放置在視圖里,然后通過定義函數訪問該視圖即可。示例腳本見下:
-- Way2 示例里引用 CREATE OR ALTER VIEW dbo.view_rand AS SELECT RAND() AS myrand; GO CREATE OR ALTER FUNCTION dbo.fun_rand() RETURNS FLOAT AS BEGIN RETURN (SELECT myrand FROM dbo.view_rand); END; GO SELECT TOP 3 empid,mgrid,empname,rand() sys_rand, dbo.fun_rand() f_rand FROM dbo.Emp A -- 結果特別需要注意的是:
1 這里調用了系統內置函數rand和自定義的rand函數,系統的函數是批查詢級別的(整個批查詢次僅執行一次rand函數),而自定義函數是記錄級別的(每條記錄都執行了rand函數)。
2 仔細查看上述腳本執行結果不能得出上述結論。
內置(非)確定值函數
綁定模式(SCHEMABINDING)選項
在持久化計算列里或者索引視圖里的自定義函數必須是確定值的。這就要求用戶自定義函數里不能調用非確定值函數并且定義時加上SCHEMABINDING選項。詳見如下例子的演示。
關于計算列:計算列分為非持久化和持久化兩種,非持久化的僅在查詢時執行且不保存計算列結果,而持久化則可以保存計算列結果且可以建立索引。示例見下:
-- 1 創建函數 CREATE OR ALTER FUNCTION dbo.fun_endyear(@datevar AS DATE) RETURNS DATE WITH SCHEMABINDING AS BEGIN RETURN DATEFROMPARTS(YEAR(@datevar), 12, 31); END; GO -- 2 創建表并制定計算列對應的函數 CREATE TABLE dbo.test ( id INT NOT NULL IDENTITY CONSTRAINT PK_test_id PRIMARY KEY, insertdate DATE NOT NULL, insertyear AS dbo.fun_endyear(insertdate) PERSISTED );特別需要說明:
1 這里針對非確定型函數需要加WITH SCHEMABINDING選項。SCHEMABINDING選項主要應用在視圖里,主要對視圖依賴的表和字段起到阻止結構變化的作用。同樣的在函數里指定該選項是為了阻斷函數依賴的表結構的變換。
2 針對計算列定義時需要加PERSISTED關鍵字。
3 關于函數DATEFROMPARTS、YEAR函數使用的說明,這里針對不同的時間格式,如dmy、myd、ymd都可以提取時間。見如下示例:
SET DATEFORMAT dmy;? GO? DECLARE @datevar DATE = '27/07/2021';? SELECT @datevar,DATEFROMPARTS(YEAR(@datevar), 12, 31);? GO? SET DATEFORMAT myd;? GO? DECLARE @datevar DATE = '07/2021/27';? SELECT @datevar,DATEFROMPARTS(YEAR(@datevar), 12, 31);? GO -- 結果均為: /* 2021-07-27? 2021-12-31 */內聯表型函數
表型函數在定義和使用上和標量值函數類似,不過這里返回的類型是表變量,即是一個結果集。針對emp員工表,按照指定的分頁和頁數,僅顯示最后一批次的數據,詳細代碼見下:
-- 1 分頁函數示例: CREATE OR ALTER FUNCTION dbo.fun_GetPage(@pagenum AS BIGINT, @pagesize AS BIGINT) RETURNS TABLE WITH SCHEMABINDING AS RETURN WITH C AS ( SELECT ROW_NUMBER() OVER(ORDER BY empid) AS rownum, empid, empname,jobtitle,salary FROM dbo.emp ) SELECT empid, empname,jobtitle,salary FROM C WHERE rownum BETWEEN (@pagenum - 1) * @pagesize + 1 AND @pagenum * @pagesize; GO -- 2 調用示例 SELECT empid, empname,jobtitle,salary FROM dbo.fun_GetPage(3,2)-- 3 結果?延展閱讀,SQL Server 2012之后版本分頁可用OFFSET FETCH語法,所以上述代碼可改寫為:
CREATE OR ALTER FUNCTION dbo.fun_GetPage_V2(@pagenum AS BIGINT, @pagesize AS BIGINT) RETURNS TABLE WITH SCHEMABINDING AS RETURN SELECT ROW_NUMBER() OVER(ORDER BY empid) AS rownum,empid, empname,jobtitle,salary FROM dbo.emp ORDER BY empid OFFSET (@pagenum - 1) * @pagesize ROWS FETCH NEXT @pagesize ROWS ONLY; GO多語句內聯表函數
多語句內聯表函數是內聯表函數的延展。內聯表一般是單個查詢,而多語句內聯表函數在頭部定義表變量后可在函數體內執行多語句對表變量進行處理((WHILE循環、插入、更新、刪除等),最終返回該表變量。
多語句內聯表應用在父子查詢時比較方便,因為我們可以方便的控制父節點。同時它可以有自己復雜的業務邏輯,而相比較而言內聯表函數只能返回一個查詢。
-- 1 多語句內聯函數(獲取級聯子節點)定義 DROP FUNCTION IF EXISTS dbo.func_GetChildtree; GO CREATE FUNCTION dbo.func_GetChildtree (@mgrid AS INT, @maxlevels AS INT = NULL) RETURNS @Tree TABLE -- 定義表變量以接收處理結果(empid INT NOT NULL PRIMARY KEY,mgrid INT NULL,empname VARCHAR(25) NOT NULL,jobtitle VARCHAR(25) NOT NULL,salary MONEY NOT NULL,lvl INT NOT NULL,sortpath VARCHAR(892) NOT NULL ,INDEX idx_lvl_empid_sortpath NONCLUSTERED(lvl, empid, sortpath))WITH SCHEMABINDING -- 綁定選項,阻止表表結構變化 AS BEGINDECLARE @lvl AS INT = 0;-- 初始化@Tree里根節點數據(參數@mgrid對應的那條數據)。INSERT INTO @Tree(empid, mgrid, empname,jobtitle, salary, lvl, sortpath)SELECT empid, NULL AS mgrid, empname,jobtitle, salary, @lvl AS lvl, '.' AS sortpathFROM dbo.EmpWHERE empid = @mgrid; -- UPDATE @Tree SET empname='五王2' WHERE mgrid IS NULL; -- 演示更新-- 借助系統變量@@ROWCOUNT(影響的行)和@maxlevels參數進入循環尋找子節點WHILE @@ROWCOUNT > 0 AND (@lvl < @maxlevels OR @maxlevels IS NULL)BEGINSET @lvl += 1;-- 通過父子關聯插入子節點數據到@Tree表INSERT INTO @Tree(empid, mgrid, empname,jobtitle ,salary, lvl, sortpath)SELECT S.empid, S.mgrid, S.empname,S.jobtitle, S.salary, @lvl AS lvl,M.sortpath + CAST(S.empid AS VARCHAR(10)) + '.' AS sortpathFROM dbo.Emp AS SINNER JOIN @Tree AS MON S.mgrid = M.empid AND M.lvl = @lvl - 1;END;RETURN; -- 返回,這里即對應開頭的定義,返回@Tree END; GO-- 2 執行 SELECT empid, REPLICATE(' | ', lvl) + empname AS empname, mgrid, salary, lvl, sortpath FROM dbo.func_GetChildtree(3, NULL) AS T ORDER BY sortpath;-- 3 結果總結
以上是生活随笔為你收集整理的T-SQL里数据库工程师都不知道的秘密之SQL Server自定义函数UDF的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql相邻行数据计算的自定义变量@和
- 下一篇: Mysql时间数据分段累加求和案例之子查