Fre*_*son 1 sql sql-server row-level-security
我正在使用 Microsoft SQL Server 2016 进行开发,目前在向数据库添加行级安全性 (RLS) 时面临性能大幅下降的问题。我已经认为我已经找到了问题所在,即查询优化器先生不太喜欢我的非确定性过滤功能。我的问题是,是否有人有 RLS、过滤功能和优化此类案例的经验。- 索引、更巧妙的RLS过滤功能等可以提高性能吗?
我使用 RLS 根据过滤器函数过滤查询中返回/可用的行。下面我设置了一个函数来根据 SESSION_CONTEXT() 函数中的变量过滤行。因此,这很像向 WHERE 子句添加过滤器(除了它不会优化相同的内容,而且这更容易应用于现有的大型应用程序,因为它是在数据库级别完成的)。
请注意,下面的脚本和测试是实际情况的非常简单的版本,但它确实表明应用过滤时性能会下降。在脚本中,我还包含(注释掉)了一些我已经尝试过的东西。
要进行设置,首先运行下面的脚本,这将创建数据库、示例表、过滤功能和安全策略。
-- 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)
Run Code Online (Sandbox Code Playgroud)
接下来继续运行以下测试。可以在 SQL Server Management Studio (SSMS) 的“消息”选项卡上查看计时,如果您想查看应用过滤步骤的位置,请务必包含实际的执行计划。
-- 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
Run Code Online (Sandbox Code Playgroud)
适用于行级安全功能的“规则”与适用于视图的“规则”大致相同,因为它们的工作方式似乎非常相似。这意味着,索引为companyid
CREATE INDEX IX_Member_OwnerCompanyId ON dbo.member (ownercompanyid)
Run Code Online (Sandbox Code Playgroud)
并重写该函数如下
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'))
Run Code Online (Sandbox Code Playgroud)
我们接近最佳结果,因为优化器独立评估两个分支,如果值为 ,则其中一个分支评估SESSION_CONTEXT为零NULL。如果不是,我们仍然会为所有与转换后的行匹配的行SESSION_CONTEXT(即那些不匹配的行NULL)进行相当昂贵的查找和合并。不过,这仍然比我的机器上的原始函数快一些,大约是非NULL.
我真的没有看到任何进一步优化的方法,尽管值得注意的是,它实际上只是昂贵,因为过滤器不是特别有选择性。此外,与具有或不具有行级安全性的普通表扫描不同SELECT COUNT(*),生成的查询不需要并行化,这进一步降低了性能。我不知道确切的问题是什么(通常内联表值函数没有问题),但即使强制使用跟踪标志 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)
Run Code Online (Sandbox Code Playgroud)
UNION ALL这次不,因为我们不想在这种情况下引发两次扫描。使用SET CONTEXT_INFO 0(“清除”它)或进行设置SET CONTEXT_INFO 1,现在查询再次变得快速,因为并行性不再受到抑制。虽然常规索引会加快速度,但现在更好的选择是列存储索引:
CREATE NONCLUSTERED COLUMNSTORE INDEX IX_Member_OwnerCompanyId ON dbo.member (ownercompanyid)
Run Code Online (Sandbox Code Playgroud)
生成的查询尽可能快,因为COUNT(*)直接从列存储提供服务,而列存储实际上就是为此而设计的。当然,在真实的应用程序(不是简单的COUNT(*))中,列存储可能会也可能不会改进,但至少它表明优化器可以使用它(如果SESSION_CONTEXT()使用则不是这种情况,因为它会回退到行处理模式)在列存储扫描之后,否定了好处)。