SQL Server(UDF)のユーザー定義関数は、各開発者が知っておくべき重要なオブジェクトです。これらは多くのシナリオ(WHERE句、計算列、およびチェック制約)で非常に役立ちますが、パフォーマンスの問題を引き起こす可能性のあるいくつかの制限と悪い習慣があります。マルチステートメントUDFは、パフォーマンスに重大な影響を与える可能性があります。この記事では、これらのシナリオについて具体的に説明します。
関数はオブジェクト指向言語と同じように実装されていませんが、パラメーター化されたビューが必要なシナリオではインラインテーブル値関数を使用できますが、これはスカラーまたはテーブルを返す関数には適用されません。これらの関数は、多くのパフォーマンスの問題を引き起こす可能性があるため、慎重に使用する必要があります。ただし、多くの場合、これらは不可欠であるため、それらの実装にさらに注意を払う必要があります。関数は、バッチ、プロシージャ、トリガーまたはビュー内のSQLステートメント、アドホックSQLクエリ内、またはPowerBIやTableauなどのツールによって生成されたレポートクエリの一部として、計算フィールドで使用され、制約をチェックします。スカラー関数は最大32レベルまで再帰的ですが、テーブル関数は再帰をサポートしていません。
SQLServerの関数の種類
SQL Serverには、3つの関数タイプがあります。単一のスカラー値を返すユーザー定義スカラー関数(SF)、テーブルを返すユーザー定義テーブル値関数(TVF)、およびインラインテーブル値関数(ITVF)です。関数本体はありません。テーブル関数は、インラインまたはマルチステートメントにすることができます。インライン関数には戻り変数はなく、値関数を返すだけです。マルチステートメント関数は、コードのBEGIN-ENDブロックに含まれており、副作用(テーブルのコンテンツの変更など)を発生させない複数のT-SQLステートメントを含めることができます。
簡単な例で各タイプの関数を示します:
/**
inline table function
**/
CREATE FUNCTION dbo.fnInline( @P1 INT, @P2 VARCHAR(50) )
RETURNS TABLE
AS
RETURN ( SELECT @P1 AS P_OUT_1, @P2 AS P_OUT_2 )
/**
multi-statement table function
**/
CREATE FUNCTION dbo.fnMultiTable( @P1 INT, @P2 VARCHAR(50) )
RETURNS @r_table TABLE ( OUT_1 INT, OUT_2 VARCHAR(50) )
AS
BEGIN
INSERT @r_table SELECT @P1, @P2;
RETURN;
END;
/**
scalar function
**/
CREATE FUNCTION dbo.fnScalar( @P1 INT, @P2 INT )
RETURNS INT
AS
BEGIN
RETURN @P1 + @P2
END
SQLServerの機能制限
はじめに述べたように、関数の使用にはいくつかの制限があります。以下でいくつか説明します。完全なリストは、 Microsoft Docsにあります。 :
- 一時的な機能の概念はありません
- 別のデータベースに関数を作成することはできませんが、権限によっては、その関数にアクセスできます
- UDFでは、データベースの状態を変更するアクションを実行することはできません。
- UDF内では、拡張ストアドプロシージャを除いて、プロシージャを呼び出すことはできません。
- UDFは結果セットを返すことはできませんが、テーブルのデータ型のみを返すことができます
- UDFで動的SQLまたは一時テーブルを使用することはできません
- UDFはエラー処理機能に制限があります。RAISERRORもTRY…CATCHもサポートしておらず、システムの@ERROR変数からデータを取得することはできません
マルチステートメント関数で許可されること
次のもののみが許可されます:
- 割り当てステートメント
- TRY…CATCHブロックを除くすべてのフロー制御ステートメント
- ローカル変数とカーソルの作成に使用されるDECLARE呼び出し
- 式を含むリストを持つSELECTクエリを使用して、これらの値をローカルで宣言された変数に割り当てることができます
- カーソルはローカルテーブルのみを参照でき、関数本体内で開いたり閉じたりする必要があります。 FETCHは、ローカル変数の値の割り当てまたは変更のみが可能であり、データベースデータの取得または変更はできません 。
許可されていても、マルチステートメント関数で避けるべきことは何ですか?
- スカラー関数で計算列を使用しているシナリオは避けてください。これにより、インデックスの再構築が発生し、再計算が必要な更新が遅くなります。
- マルチステートメント関数には実行プランとパフォーマンスへの影響があることを考慮してください
- マルチステートメントのテーブル値UDFは、SQL式または結合ステートメントで使用されると、実行プランが最適でないために遅くなります
- 小さなデータセットをクエリすることが確実で、そのデータセットが将来も小さいままであることが確実でない限り、WHEREステートメントとON句でスカラー関数を使用しないでください
関数名とパラメーター
他のオブジェクト名と同様に、関数名は識別子の規則に準拠する必要があり、スキーマ内で一意である必要があります。スカラー関数を作成している場合は、EXECUTEステートメントを使用して実行できます。この場合、関数名にスキーマ名を入れる必要はありません。以下のEXECUTE関数呼び出しの例を参照してください(月のN日目の発生を返し、このデータを取得する関数を作成します):
CREATE FUNCTION dbo.fnGetDayofWeekInMonth
(
@YearInput VARCHAR(50),
@MonthInput VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
@WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
@CountN INT=1 -- 1 for the first date, 2 for the second occurrence, 3 for the third
)
RETURNS DATETIME
AS
BEGIN
RETURN DATEADD(MONTH, DATEDIFF(MONTH, 0,
CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
-
(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0,
CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
[email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7
END
-- In SQL Server 2012 and later versions, you can use the EXECUTE command or the SELECT command to run a UDF, or use a standard approach
DECLARE @ret DateTime
EXEC @ret = fnGetDayofWeekInMonth '2020', 'Jan', 'Mon',2
SELECT @ret AS Third_Monday_In_January_2020
SELECT dbo.fnGetDayofWeekInMonth('2020', 'Jan', DEFAULT, DEFAULT)
AS 'Using default',
dbo.fnGetDayofWeekInMonth('2020', 'Jan', 'Mon', 2) AS 'explicit'
関数パラメーターのデフォルトを定義できます。これらのパラメーターには、接頭辞「@」を付け、識別子の命名規則に準拠する必要があります。パラメータは定数値のみにすることができ、テーブル、ビュー、列、またはその他のデータベースオブジェクトの代わりにSQLクエリで使用することはできません。また、値を式にすることはできません。 TIMESTAMPデータ型を除いて、すべてのデータ型が許可され、テーブル値パラメーターを除いて、非スカラーデータ型を使用することはできません。 「標準」関数呼び出しでは、エンドユーザーがパラメーターをオプションにすることができるようにする場合は、DEFAULT属性を指定する必要があります。新しいバージョンでは、EXECUTE構文を使用して、これは不要になりました。関数呼び出しでこのパラメーターを入力しないでください。カスタムテーブルタイプを使用している場合は、読み取り専用としてマークする必要があります。つまり、関数内で初期値を変更することはできませんが、他のパラメーターの計算や定義に使用できます。
SQLServer機能のパフォーマンス
この記事で取り上げる最後のトピックは、前の章の関数を使用して、関数のパフォーマンスです。この機能を拡張し、実行時間と実行計画の品質を監視します。他の関数バージョンを作成することから始めて、それらの比較を続けます:
CREATE FUNCTION dbo.fnGetDayofWeekInMonthBound
(
@YearInput VARCHAR(50),
@MonthInput VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
@WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
@CountN INT=1 -- 1 for the first date, 2 for the second occurrence, 3 for the third
)
RETURNS DATETIME
WITH SCHEMABINDING
AS
BEGIN
RETURN DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
-(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
[email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7
END
GO
CREATE FUNCTION dbo.fnNthDayOfWeekOfMonthInline (
@YearInput VARCHAR(50),
@MonthInput VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
@WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
@CountN INT=1 -- 1 for the first date, 2 for the second occurence, 3 for the third
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN (SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
-(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
[email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7 AS TheDate)
GO
CREATE FUNCTION dbo.fnNthDayOfWeekOfMonthTVF (
@YearInput VARCHAR(50),
@MonthInput VARCHAR(50), -- English months ( 'Jan', 'Feb', ... )
@WeekDayInput VARCHAR(50)='Mon', -- Mon, Tue, Wed, Thu, Fri, Sat, Sun
@CountN INT=1 -- 1 for the first date, 2 for the second occurence, 3 for the third
)
RETURNS @When TABLE (TheDate DATETIME)
WITH schemabinding
AS
Begin
INSERT INTO @When(TheDate)
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0)+ (7*@CountN)-1
-(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '[email protected]+' '[email protected],113)), 0))
[email protected]@DateFirst+(CHARINDEX(@WeekDayInput,'FriThuWedTueMonSunSat')-1)/3)%7
RETURN
end
GO
いくつかのテストコールとテストケースを作成する
テーブルバージョンから始めます:
SELECT * FROM dbo.fnNthDayOfWeekOfMonthTVF('2020','Feb','Tue',2)
SELECT TheYear, CONVERT(NCHAR(11),(SELECT TheDate FROM dbo.fnNthDayOfWeekOfMonthTVF(TheYear,'Feb','Tue',2)),113) FROM (VALUES ('2014'),('2015'),('2016'),('2017'),('2018'),('2019'),('2020'),('2021'))years(TheYear)
SELECT TheYear, CONVERT(NCHAR(11),TheDate,113) FROM (VALUES ('2014'),('2015'),('2016'),('2017'),('2018'),('2019'),('2020'),('2021'))years(TheYear)
OUTER apply dbo.fnNthDayOfWeekOfMonthTVF(TheYear,'Feb','Tue',2)
テストデータの作成:
IF EXISTS(SELECT * FROM tempdb.sys.tables WHERE name LIKE '#DataForTest%')
DROP TABLE #DataForTest
GO
SELECT *
INTO #DataForTest
FROM (VALUES ('2014'),('2015'),('2016'),('2017'),('2018'),('2019'),('2020'),('2021'))years(TheYear)
CROSS join (VALUES ('jan'),('feb'),('mar'),('apr'),('may'),('jun'),('jul'),('aug'),('sep'),('oct'),('nov'),('dec'))months(Themonth)
CROSS join (VALUES ('Mon'),('Tue'),('Wed'),('Thu'),('Fri'),('Sat'),('Sun'))day(TheDay)
CROSS join (VALUES (1),(2),(3),(4))nth(nth)
テストパフォーマンス:
DECLARE @TableLog TABLE (OrderVal INT IDENTITY(1,1), Reason VARCHAR(500), TimeOfEvent DATETIME2 DEFAULT GETDATE())
タイミングの開始:
INSERT INTO @TableLog(Reason) SELECT 'Starting My_Section_of_code' --place at the start
まず、ベースラインを取得するために関数のタイプを使用しません:
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '+TheMonth+' '+TheYear,113)), 0)+ (7*Nth)-1
-(DATEPART (WEEKDAY, DATEADD(MONTH, DATEDIFF(MONTH, 0, CONVERT(DATE,'1 '+TheMonth+' '+TheYear,113)), 0))
[email protected]@DateFirst+(CHARINDEX(TheDay,'FriThuWedTueMonSunSat')-1)/3)%7 AS TheDate
INTO #Test0
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Using the code entirely unwrapped';
クロスアプライされたインラインテーブル値関数を使用するようになりました:
SELECT TheYear, CONVERT(NCHAR(11),TheDate,113) AS itsdate
INTO #Test1
FROM #DataForTest
CROSS APPLY dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)
INSERT INTO @TableLog(Reason) SELECT 'Inline function cross apply'
クロスアプライされたインラインテーブル値関数を使用します:
SELECT TheYear, CONVERT(NCHAR(11),(SELECT TheDate FROM dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)),113) AS itsDate
INTO #Test2
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Inline function Derived table'
信頼できないものを比較するために、schemabindingを使用したスカラー関数を使用します:
SELECT TheYear, CONVERT(NCHAR(11), dbo.fnGetDayofWeekInMonthBound(TheYear,TheMonth,TheDay,nth))itsdate
INTO #Test3
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Trusted (Schemabound) scalar function'
次に、スキーマバインディングなしでスカラー関数を使用します:
SELECT TheYear, CONVERT(NCHAR(11), dbo.fnGetDayofWeekInMonth(TheYear,TheMonth,TheDay,nth))itsdate
INTO #Test6
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'Untrusted scalar function'
次に、次のように派生したマルチステートメントテーブル関数:
SELECT TheYear, CONVERT(NCHAR(11),(SELECT TheDate FROM dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)),113) AS itsdate
INTO #Test4
FROM #DataForTest
INSERT INTO @TableLog(Reason) SELECT 'multi-statement table function derived'
最後に、マルチステートメントテーブルが相互適用されました:
SELECT TheYear, CONVERT(NCHAR(11),TheDate,113) AS itsdate
INTO #Test5
FROM #DataForTest
CROSS APPLY dbo.fnNthDayOfWeekOfMonthTVF(TheYear,TheMonth,TheDay,nth)
INSERT INTO @TableLog(Reason) SELECT 'multi-statement cross APPLY'--where the routine you want to time ends
すべてのタイミングをリストアップします:
SELECT ending.Reason AS Test, DateDiff(ms, starting.TimeOfEvent,ending.TimeOfEvent) [AS Time (ms)] FROM @TableLog starting
INNER JOIN @TableLog ending ON ending.OrderVal=starting.OrderVal+1
DROP table #Test0
DROP table #Test1
DROP table #Test2
DROP table #Test3
DROP table #Test4
DROP table #Test5
DROP table #Test6
DROP TABLE #DataForTest
上記の表は、ユーザー定義関数を使用する場合は、パフォーマンスと機能を考慮する必要があることを明確に示しています。
結論
関数は、主に「論理構造」であるため、多くの開発者に好まれています。テストケースは簡単に作成できます。テストケースは決定論的でカプセル化されており、SQLコードフローとうまく統合され、パラメーター化の柔軟性を実現します。これらは、複数のシナリオで再利用する必要がある、より小さなデータセットまたは既にフィルタリングされたデータセットで実行する必要がある複雑なロジックを実装する必要がある場合に適しています。インラインテーブルビューは、特に上位層(クライアント向けアプリケーション)からのパラメーターを必要とするビューで使用できます。一方、スカラー関数は再帰的に呼び出すことができるため、XMLやその他の階層形式での作業に最適です。
ユーザー定義のマルチステートメント関数は、開発ツールスタックへの優れた追加機能ですが、それらがどのように機能し、それらの制限とパフォーマンスの課題が何であるかを理解する必要があります。これらを誤って使用すると、データベースのパフォーマンスが低下する可能性がありますが、これらの関数の使用方法を知っている場合は、コードの再利用とカプセル化に多くのメリットをもたらす可能性があります。