Сначала ID: это самое избирательное (то есть самое уникальное) поле. Но, будучи полем автоматического увеличения (или случайным, если все еще используют GUID), данные каждого клиента распределяются по каждой таблице. Это означает, что иногда клиенту требуется 100 строк, а для этого требуется почти 100 страниц данных, считываемых с диска (не быстро) в буферный пул (занимающих больше места, чем 10 страниц данных). Это также увеличивает конкуренцию на страницах данных, так как более частым потребностям нескольких пользователей потребуется обновить одну и ту же страницу данных.
Однако обычно вы не сталкиваетесь с таким количеством проблем, связанных с анализом параметров / плохим кэшированием плана, а с тем, что статистика по различным значениям идентификаторов довольно непротиворечива. Возможно, вы не получите самые оптимальные планы, но у вас будет меньше шансов получить ужасные. Этот метод существенно жертвует производительностью (незначительно) для всех клиентов, чтобы получить выгоду от менее частых проблем.
TenantID первый:Это очень не избирательно вообще. Может быть очень мало вариаций на 1 миллион строк, если у вас есть только 100 TenantID. Но статистика для этих запросов является более точной, поскольку SQL Server будет знать, что запрос для Арендатора A отзовет 500 000 строк, но этот же запрос для Арендатора B - только 50 строк. Вот где главная болевая точка. Этот метод значительно повышает вероятность возникновения проблем с отслеживанием параметров, когда первый запуск хранимой процедуры выполняется для арендатора A и действует соответствующим образом на основе оптимизатора запросов, который просматривает эту статистику и знает, что она должна быть эффективной для получения строк по 500 тыс. Но когда запускается Арендатор B, имеющий только 50 строк, этот план выполнения больше не подходит и, на самом деле, совершенно неуместен. И, так как данные не вставляются в порядке ведущего поля,
Однако для первого TenantID, выполняющего хранимую процедуру, производительность должна быть выше, чем в другом подходе, поскольку данные (по крайней мере, после обслуживания индекса) будут физически и логически организованы таким образом, что для удовлетворения требований потребуется гораздо меньше страниц данных. запросы. Это означает, что меньше физического ввода-вывода, меньше логических чтений, меньше конфликтов между арендаторами за одни и те же страницы данных, меньше потраченного впустую пространства в буферном пуле (следовательно, улучшенная продолжительность жизни страниц) и т. Д.
Есть две основные затраты для получения этой улучшенной производительности. Первое не так сложно: вы должны регулярно выполнять обслуживание индекса, чтобы противостоять усилению фрагментации. Второе немного менее весело.
Чтобы противодействовать возросшим проблемам отслеживания параметров, необходимо разделить планы выполнения между арендаторами. Упрощенный подход заключается в использовании WITH RECOMPILE
на процессах или OPTION (RECOMPILE)
подсказке запроса, но это удар по производительности, который может уничтожить все выгоды, достигнутые на TenantID
первом месте. Метод, который я нашел, работал лучше всего - использовать параметризованный динамический SQL через sp_executesql
. Причина, по которой нужен динамический SQL, заключается в том, чтобы разрешить объединение TenantID в текст запроса, в то время как все другие предикаты, которые обычно являются параметрами, по-прежнему являются параметрами. Например, если вы искали определенный ордер, вы бы сделали что-то вроде:
DECLARE @GetOrderSQL NVARCHAR(MAX);
SET @GetOrderSQL = N'
SELECT ord.field1, ord.field2, etc.
FROM dbo.Orders ord
WHERE ord.TenantID = ' + CONVERT(NVARCHAR(10), @TenantID) + N'
AND ord.OrderID = @OrderID_dyn;
';
EXEC sp_executesql
@GetOrderSQL,
N'@OrderID_dyn INT',
@OrderID_dyn = @OrderID;
В результате создается план запроса многократного использования только для этого TenantID, который будет соответствовать объему данных этого конкретного Tenant. Если тот же Арендатор A снова выполнит хранимую процедуру для другой, @OrderID
он будет повторно использовать этот кэшированный план запросов. Другой Арендатор, выполняющий ту же Хранимую Процедуру, сгенерирует текст запроса, который отличается только по значению TenantID, но любой разницы в тексте запроса достаточно для создания другого плана. И план, сгенерированный для Арендатора B, будет не только соответствовать объему данных для Арендатора B, но также будет многократно использоваться для Арендатора B для различных значений @OrderID
(так как этот предикат все еще параметризован).
Недостатками этого подхода являются:
- Это немного больше работы, чем просто ввод простого запроса (но не все запросы должны быть Dynamic SQL, только те, которые заканчиваются проблемой перехвата параметров).
- В зависимости от того, сколько Арендаторов в системе, это увеличивает размер кэша планов, поскольку для каждого запроса теперь требуется 1 план на каждый вызывающий его TenantID. Это не может быть проблемой, но, по крайней мере, о чем-то нужно знать.
Динамический SQL разрывает цепочку владения, что означает, что доступ на чтение / запись к таблицам не может быть получен при наличии EXECUTE
разрешения на хранимую процедуру. Простое, но менее безопасное решение - дать пользователю прямой доступ к таблицам. Это, конечно, не идеально, но обычно это быстрый и легкий компромисс. Более безопасный подход заключается в использовании безопасности на основе сертификатов. То есть, создать сертификат, затем создать пользователя из этого сертификата, предоставить этому пользователю необходимые разрешения (пользователь или логин на основе сертификата не могут подключиться к SQL Server самостоятельно), а затем подписать хранимые процедуры, использующие динамический SQL, с этим тот же сертификат через ДОБАВИТЬ ПОДПИСЬ .
Для получения дополнительной информации о подписании модулей и сертификатах, пожалуйста, смотрите: ModuleSigning.Info
(ID, TenantID)
и вы также создаете некластеризованный индекс(TenantID, ID)
или просто(TenantID)
хотите получать точную статистику для запросов, которые обрабатывают большинство строк одного арендатора?