SQL Server 2016 с безопасностью на уровне строк - устранение узких мест - PullRequest
1 голос
/ 20 января 2020

Я работаю на Microsoft SQL Server 2016, и в настоящее время наблюдается серьезное падение производительности при добавлении Row Level Security (RLS) в мою базу данных. Я уже думаю, что нашел проблему, которой является Mr Query Optimizer , которой очень не нравится моя недетерминированная c функция фильтрации. Мой вопрос: есть ли у кого-нибудь опыт работы с RLS, функциями фильтрации и оптимизации подобного случая. - Может ли индексирование, более умная функция фильтрации RLS и т. Д. c улучшить производительность?

Я использую RLS для фильтрации возвращенных / доступных строк из запроса на основе функции фильтра. Ниже я настраиваю функцию для фильтрации строк на основе переменной из функции SESSION_CONTEXT (). Так что это похоже на добавление фильтра к предложению WHERE (за исключением того, что он не оптимизирует то же самое, и его гораздо проще применить к существующему огромному приложению, поскольку это делается на уровне базы данных).

Обратите внимание, что приведенный ниже скрипт и тесты - очень упрощенная c версия реальной вещи, но она демонстрирует падение производительности при применении фильтрации. В сценарии я также включил (закомментировал) некоторые вещи, которые я уже пробовал.

Для установки сначала запустите приведенный ниже скрипт, , который создаст базу данных, образец таблицы, функцию фильтрации и политику безопасности.

-- 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) 

Далее * На 1024 * вперед и запустите следующие тесты . Время можно просмотреть на вкладке «Сообщения» в 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

1 Ответ

3 голосов
/ 20 января 2020

Для функций безопасности на уровне строк применяются те же "правила", что и для представлений, так как они, похоже, работают аналогичным образом. Это означает, что с индексом 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(*) и без защиты на уровне строк, результирующий запрос не хочет распараллеливать, что еще больше снижает производительность. Я не знаю, в чем именно заключается проблема (обычно встроенные табличные функции не являются проблемой), но даже форсирование с помощью флага трассировки 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 (чтобы "очистить" его), либо SET CONTEXT_INFO 1, и теперь запросы снова выполняются быстро, поскольку параллелизм больше не запрещается. И хотя обычный индекс ускорит это, еще лучшим вариантом теперь является индекс columnstore:

CREATE NONCLUSTERED COLUMNSTORE INDEX IX_Member_OwnerCompanyId ON dbo.member (ownercompanyid)

Результирующие запросы выполняются настолько быстро, насколько это возможно, так как COUNT(*) напрямую подается из columnstore, который практически сделан для этого. Конечно, в реальном приложении (а не в простом COUNT(*)) хранилище columns может или не может улучшить ситуацию, но, по крайней мере, оно демонстрирует, что оптимизатор может его использовать (что не так, если используется SESSION_CONTEXT(), когда он падает). обратно в режим обработки строк сразу после сканирования хранилища столбцов, сводя на нет преимущества).

...