SQL 具有行级安全性的 Server 2016 - 解决瓶颈
SQL Server 2016 with Row Level Security - Addressing the Bottleneck
我正在使用 Microsoft SQL Server 2016 进行开发,在向我的数据库添加行级安全性 (RLS) 时,目前正面临着严重的性能下降。我认为我已经找到了问题所在,即 Mr Query Optimizer 非常不喜欢我的非确定性过滤功能。我的问题是是否有人对 RLS、过滤功能和优化这样的案例有任何经验。 - 索引、更智能的 RLS 过滤功能等能否提高性能?
I use RLS to filter the returned/available rows from a query based on a filter function. Below I setup a function to filter rows based on a variable from the SESSION_CONTEXT() function. So it's much like adding a filter to the WHERE clause (except that it doesn't optimize the same, and this is way easier to apply to an existing huge application, since it's done on the database level).
请注意,下面的脚本和测试是实际情况的非常简单的版本,但它确实表明应用过滤后性能会下降。在脚本中,我还包括(注释掉)了一些我已经尝试过的东西。
要设置,首先运行下面的脚本,这将创建数据库、示例table、过滤功能和安全策略。
-- note: this creates the test database 'rlstest'. when you're tired of this, just drop it.
-- initalize
SET NOCOUNT ON
GO
-- create database
CREATE DATABASE rlstest
GO
-- set database
USE rlstest
GO
-- create test table 'member'
CREATE TABLE dbo.member (
memberid INT NOT NULL IDENTITY,
ownercompanyid INT NULL
)
GO
-- create some sample rows where dbo.member.ownercompanyid is sometimes 1 and sometimes NULL
-- note 1:
-- below, adjust the number of rows to create to give you testresults between 1-10 seconds (so that you notice the drop of performance)
-- about 2million rows gives me a test result (with the security policy) of about 0,5-1sec on an average dev machine
-- note 2: transaction is merly to give some speed to this
BEGIN TRY
BEGIN TRAN
DECLARE @x INT = 2000000
WHILE @x > 0 BEGIN
INSERT dbo.member (ownercompanyid) VALUES (CASE WHEN FLOOR(RAND()*2+1)>1 THEN 1 ELSE NULL END)
SET @x = @x - 1
END
COMMIT TRAN
END TRY BEGIN CATCH
ROLLBACK
END CATCH
GO
-- drop policy & filter function
-- DROP SECURITY POLICY dbo.OwnerCompanyDataSecurityPolicy
-- DROP FUNCTION dbo.fn_filterMember
-- create filter function
CREATE FUNCTION dbo.fn_filterMember(@ownercompanyid AS INT) RETURNS TABLE WITH SCHEMABINDING AS
RETURN SELECT 1 result WHERE
@ownercompanyid IS NULL OR
(@ownercompanyid IS NOT NULL AND @ownercompanyid=CAST(SESSION_CONTEXT(N'companyid') AS INT))
-- tested: short circuit the logical expression (no luck):
-- @ownercompanyid IS NULL OR
-- (CASE WHEN @ownercompanyid IS NOT NULL THEN (CASE WHEN @ownercompanyid=CAST(SESSION_CONTEXT(N'companyid') AS INT) THEN 1 ELSE 0 END) ELSE 0 END)=1
GO
-- create & activate security policy
CREATE SECURITY POLICY dbo.OwnerCompanyDataSecurityPolicy
ADD FILTER PREDICATE dbo.fn_filterMember(ownercompanyid) ON dbo.member
WITH (STATE = ON)
接下来继续运行下面的测试。可以在 SQL Server Management Studio (SSMS) 中的 "Messages" 选项卡上查看计时,如果您想查看应用过滤步骤的位置,请确保包含实际的执行计划。
-- tested: add a table index (no luck)
-- CREATE INDEX ix_member_test ON dbo.member (ownercompanyid)
-- test without security policy
ALTER SECURITY POLICY dbo.OwnerCompanyDataSecurityPolicy
WITH (STATE = OFF)
-- note: view timings on the "Messages" tab in SSMS
SET STATISTICS TIME ON
PRINT '*** Test #1 WITHOUT security policy. Session companyid=NULL:'
EXEC sys.sp_set_session_context @key='companyid',@value=NULL
SELECT COUNT(*) FROM member
PRINT '*** Test #2 WITHOUT security policy. Session companyid=1:'
EXEC sys.sp_set_session_context @key='companyid',@value=1
SELECT COUNT(*) FROM member
SET STATISTICS TIME OFF
-- test with security policy
ALTER SECURITY POLICY dbo.OwnerCompanyDataSecurityPolicy
WITH (STATE = ON)
SET STATISTICS TIME ON
PRINT '*** Test #3 WITH security policy. Session companyid=NULL:'
EXEC sys.sp_set_session_context @key='companyid',@value=NULL
SELECT COUNT(*) FROM member
PRINT '*** Test #4 WITH security policy. Session companyid=1:'
EXEC sys.sp_set_session_context @key='companyid',@value=1
SELECT COUNT(*) FROM member
SET STATISTICS TIME OFF
与视图一样,"rules" 适用于行级安全功能,因为它们似乎以一种奇怪的类似方式工作。这意味着,索引 companyid
CREATE INDEX IX_Member_OwnerCompanyId ON dbo.member (ownercompanyid)
并重写函数如下
CREATE FUNCTION dbo.fn_filterMember(@ownercompanyid AS INT)
RETURNS TABLE
WITH SCHEMABINDING AS
RETURN
SELECT 1 AS result
WHERE @ownercompanyid IS NULL
UNION ALL
SELECT 1
WHERE @ownercompanyid = CONVERT(INT, SESSION_CONTEXT(N'companyid'))
我们接近最佳结果,因为优化器独立评估两个分支,如果 SESSION_CONTEXT
值为 NULL
,其中一个分支评估为空。如果不是,我们仍然会为所有匹配转换后的 SESSION_CONTEXT
的行(即不是 NULL
的行)进行相当昂贵的查找和合并。不过,这仍然比我机器上的原始函数快一点,大约不是 NULL
.
的行的比例
我真的看不出有什么方法可以进一步优化它,尽管值得注意的是它真的很贵,因为过滤器不是特别有选择性。此外,与具有 SELECT COUNT(*)
且没有行级安全性的普通 table 扫描不同,生成的查询不想并行化,这进一步阻碍了性能。我不知道确切的问题是什么(通常内联 table 值函数没有问题),但即使强制使用跟踪标志 8649 也无济于事。这似乎是行级安全功能的普遍问题,因为即使是索引 (WHERE @ownercompanyid IS NULL
) 支持的微不足道的常量过滤器在某些情况下也会抑制并行性。
如果您没有与 SESSION_CONTEXT
结婚,实际上还有一个更快的选择:它的哥哥 CONTEXT_INFO
。
CONTEXT_INFO
的缺点正是发明SESSION_CONTEXT
的原因,因为它是一个单一的全局(所以不同的应用程序很容易踩到对方的脚),它有一个固定的类型BINARY(128) NOT NULL
,它不能被保护(所以不受信任的应用程序可以清除它)并且它只能用 SET CONTEXT_INFO
设置,它不接受表达式或变量。
尽管如此,如果使用 CONTEXT_INFO
是一个值得考虑的选项,因为优化器比它的键控对手更喜欢它。即:
CREATE FUNCTION dbo.fn_filterMember(@ownercompanyid AS INT)
RETURNS TABLE
WITH SCHEMABINDING AS
RETURN
SELECT 1 _
WHERE @ownercompanyid IS NULL
OR @ownercompanyid = NULLIF(CONVERT(INT, CONVERT(BINARY(4), CONTEXT_INFO())), 0)
这次没有UNION ALL
,因为我们不想在这种情况下进行两次扫描。设置为 SET CONTEXT_INFO 0
(改为 "clear")或 SET CONTEXT_INFO 1
,现在查询速度再次变快,因为不再抑制并行性。虽然常规索引会加快速度,但现在更好的选择是列存储索引:
CREATE NONCLUSTERED COLUMNSTORE INDEX IX_Member_OwnerCompanyId ON dbo.member (ownercompanyid)
生成的查询尽可能快,因为 COUNT(*)
直接从列存储提供服务,这实际上是为此而设计的。当然,在实际应用程序(不是简单的 COUNT(*)
)中,列存储可能会改进也可能不会改进,但至少它证明了优化器可以使用它(如果使用 SESSION_CONTEXT()
则情况并非如此,因为它在列存储扫描后立即回退到行处理模式,从而抵消了好处)。
我正在使用 Microsoft SQL Server 2016 进行开发,在向我的数据库添加行级安全性 (RLS) 时,目前正面临着严重的性能下降。我认为我已经找到了问题所在,即 Mr Query Optimizer 非常不喜欢我的非确定性过滤功能。我的问题是是否有人对 RLS、过滤功能和优化这样的案例有任何经验。 - 索引、更智能的 RLS 过滤功能等能否提高性能?
I use RLS to filter the returned/available rows from a query based on a filter function. Below I setup a function to filter rows based on a variable from the SESSION_CONTEXT() function. So it's much like adding a filter to the WHERE clause (except that it doesn't optimize the same, and this is way easier to apply to an existing huge application, since it's done on the database level).
请注意,下面的脚本和测试是实际情况的非常简单的版本,但它确实表明应用过滤后性能会下降。在脚本中,我还包括(注释掉)了一些我已经尝试过的东西。
要设置,首先运行下面的脚本,这将创建数据库、示例table、过滤功能和安全策略。
-- note: this creates the test database 'rlstest'. when you're tired of this, just drop it.
-- initalize
SET NOCOUNT ON
GO
-- create database
CREATE DATABASE rlstest
GO
-- set database
USE rlstest
GO
-- create test table 'member'
CREATE TABLE dbo.member (
memberid INT NOT NULL IDENTITY,
ownercompanyid INT NULL
)
GO
-- create some sample rows where dbo.member.ownercompanyid is sometimes 1 and sometimes NULL
-- note 1:
-- below, adjust the number of rows to create to give you testresults between 1-10 seconds (so that you notice the drop of performance)
-- about 2million rows gives me a test result (with the security policy) of about 0,5-1sec on an average dev machine
-- note 2: transaction is merly to give some speed to this
BEGIN TRY
BEGIN TRAN
DECLARE @x INT = 2000000
WHILE @x > 0 BEGIN
INSERT dbo.member (ownercompanyid) VALUES (CASE WHEN FLOOR(RAND()*2+1)>1 THEN 1 ELSE NULL END)
SET @x = @x - 1
END
COMMIT TRAN
END TRY BEGIN CATCH
ROLLBACK
END CATCH
GO
-- drop policy & filter function
-- DROP SECURITY POLICY dbo.OwnerCompanyDataSecurityPolicy
-- DROP FUNCTION dbo.fn_filterMember
-- create filter function
CREATE FUNCTION dbo.fn_filterMember(@ownercompanyid AS INT) RETURNS TABLE WITH SCHEMABINDING AS
RETURN SELECT 1 result WHERE
@ownercompanyid IS NULL OR
(@ownercompanyid IS NOT NULL AND @ownercompanyid=CAST(SESSION_CONTEXT(N'companyid') AS INT))
-- tested: short circuit the logical expression (no luck):
-- @ownercompanyid IS NULL OR
-- (CASE WHEN @ownercompanyid IS NOT NULL THEN (CASE WHEN @ownercompanyid=CAST(SESSION_CONTEXT(N'companyid') AS INT) THEN 1 ELSE 0 END) ELSE 0 END)=1
GO
-- create & activate security policy
CREATE SECURITY POLICY dbo.OwnerCompanyDataSecurityPolicy
ADD FILTER PREDICATE dbo.fn_filterMember(ownercompanyid) ON dbo.member
WITH (STATE = ON)
接下来继续运行下面的测试。可以在 SQL Server Management Studio (SSMS) 中的 "Messages" 选项卡上查看计时,如果您想查看应用过滤步骤的位置,请确保包含实际的执行计划。
-- tested: add a table index (no luck)
-- CREATE INDEX ix_member_test ON dbo.member (ownercompanyid)
-- test without security policy
ALTER SECURITY POLICY dbo.OwnerCompanyDataSecurityPolicy
WITH (STATE = OFF)
-- note: view timings on the "Messages" tab in SSMS
SET STATISTICS TIME ON
PRINT '*** Test #1 WITHOUT security policy. Session companyid=NULL:'
EXEC sys.sp_set_session_context @key='companyid',@value=NULL
SELECT COUNT(*) FROM member
PRINT '*** Test #2 WITHOUT security policy. Session companyid=1:'
EXEC sys.sp_set_session_context @key='companyid',@value=1
SELECT COUNT(*) FROM member
SET STATISTICS TIME OFF
-- test with security policy
ALTER SECURITY POLICY dbo.OwnerCompanyDataSecurityPolicy
WITH (STATE = ON)
SET STATISTICS TIME ON
PRINT '*** Test #3 WITH security policy. Session companyid=NULL:'
EXEC sys.sp_set_session_context @key='companyid',@value=NULL
SELECT COUNT(*) FROM member
PRINT '*** Test #4 WITH security policy. Session companyid=1:'
EXEC sys.sp_set_session_context @key='companyid',@value=1
SELECT COUNT(*) FROM member
SET STATISTICS TIME OFF
与视图一样,"rules" 适用于行级安全功能,因为它们似乎以一种奇怪的类似方式工作。这意味着,索引 companyid
CREATE INDEX IX_Member_OwnerCompanyId ON dbo.member (ownercompanyid)
并重写函数如下
CREATE FUNCTION dbo.fn_filterMember(@ownercompanyid AS INT)
RETURNS TABLE
WITH SCHEMABINDING AS
RETURN
SELECT 1 AS result
WHERE @ownercompanyid IS NULL
UNION ALL
SELECT 1
WHERE @ownercompanyid = CONVERT(INT, SESSION_CONTEXT(N'companyid'))
我们接近最佳结果,因为优化器独立评估两个分支,如果 SESSION_CONTEXT
值为 NULL
,其中一个分支评估为空。如果不是,我们仍然会为所有匹配转换后的 SESSION_CONTEXT
的行(即不是 NULL
的行)进行相当昂贵的查找和合并。不过,这仍然比我机器上的原始函数快一点,大约不是 NULL
.
我真的看不出有什么方法可以进一步优化它,尽管值得注意的是它真的很贵,因为过滤器不是特别有选择性。此外,与具有 SELECT COUNT(*)
且没有行级安全性的普通 table 扫描不同,生成的查询不想并行化,这进一步阻碍了性能。我不知道确切的问题是什么(通常内联 table 值函数没有问题),但即使强制使用跟踪标志 8649 也无济于事。这似乎是行级安全功能的普遍问题,因为即使是索引 (WHERE @ownercompanyid IS NULL
) 支持的微不足道的常量过滤器在某些情况下也会抑制并行性。
如果您没有与 SESSION_CONTEXT
结婚,实际上还有一个更快的选择:它的哥哥 CONTEXT_INFO
。
CONTEXT_INFO
的缺点正是发明SESSION_CONTEXT
的原因,因为它是一个单一的全局(所以不同的应用程序很容易踩到对方的脚),它有一个固定的类型BINARY(128) NOT NULL
,它不能被保护(所以不受信任的应用程序可以清除它)并且它只能用 SET CONTEXT_INFO
设置,它不接受表达式或变量。
尽管如此,如果使用 CONTEXT_INFO
是一个值得考虑的选项,因为优化器比它的键控对手更喜欢它。即:
CREATE FUNCTION dbo.fn_filterMember(@ownercompanyid AS INT)
RETURNS TABLE
WITH SCHEMABINDING AS
RETURN
SELECT 1 _
WHERE @ownercompanyid IS NULL
OR @ownercompanyid = NULLIF(CONVERT(INT, CONVERT(BINARY(4), CONTEXT_INFO())), 0)
这次没有UNION ALL
,因为我们不想在这种情况下进行两次扫描。设置为 SET CONTEXT_INFO 0
(改为 "clear")或 SET CONTEXT_INFO 1
,现在查询速度再次变快,因为不再抑制并行性。虽然常规索引会加快速度,但现在更好的选择是列存储索引:
CREATE NONCLUSTERED COLUMNSTORE INDEX IX_Member_OwnerCompanyId ON dbo.member (ownercompanyid)
生成的查询尽可能快,因为 COUNT(*)
直接从列存储提供服务,这实际上是为此而设计的。当然,在实际应用程序(不是简单的 COUNT(*)
)中,列存储可能会改进也可能不会改进,但至少它证明了优化器可以使用它(如果使用 SESSION_CONTEXT()
则情况并非如此,因为它在列存储扫描后立即回退到行处理模式,从而抵消了好处)。