具有行级安全性的 SQL Server 2016 - 解决瓶颈

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)

Jer*_*ert 5

适用于行级安全功能的“规则”与适用于视图的“规则”大致相同,因为它们的工作方式似乎非常相似。这意味着,索引为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()使用则不是这种情况,因为它会回退到行处理模式)在列存储扫描之后,否定了好处)。