Перший ідентифікатор: Це найбільш селективне (тобто найбільш унікальне) поле. Але, будучи полем автоматичного збільшення (або випадковим, якщо все ще використовуються GUID), дані кожного клієнта поширюються по всій таблиці. Це означає, що бувають випадки, коли клієнту потрібно 100 рядків, і для цього потрібно майже 100 сторінок даних, які читаються з диска (не швидко) в буферний пул (займаючи більше місця, ніж 10 сторінок даних). Це також збільшує суперечки на сторінках даних, оскільки буде частіше, що кілька клієнтів потребуватимуть оновлення однієї сторінки даних.
Однак, як правило, ви не стикаєтеся з такою кількістю проблем щодо планування нюху / поганого кешування параметрів, оскільки статистика для різних значень ідентифікатора є досить послідовною. Можливо, ви не отримаєте найоптимальніших планів, але у вас буде менше шансів отримати жахливі. Цей метод, по суті, жертвує ефективністю для всіх клієнтів, щоб отримати користь від менш частих проблем.
Перший орендар:Це зовсім не вибірково. У 1 мільйон рядків може бути дуже мало варіацій, якщо у вас є лише 100 TenantID. Але статистика цих запитів є більш точною, оскільки SQL Server буде знати, що запит для Орендаря А поверне 500 000 рядків, але той самий запит для Тенатора Б становить лише 50 рядків. Саме тут головна больова точка. Цей метод значно збільшує шанси виникнення проблем з обнюхуванням параметрів, коли перший запуск збереженої процедури призначений для орендаря А і діє належним чином на основі Оптимізатора запитів, який бачить цю статистику і знаючи, що вона повинна бути ефективною, отримуючи 500 к рядків. Але коли орендатор B має лише 50 рядів, то план виконання більше не підходить, а насправді є зовсім недоречним. ТА, оскільки дані не вставляються в порядку головного поля,
Однак для першого TenantID, який запустив збережену процедуру, продуктивність повинна бути кращою, ніж в іншому підході, оскільки дані (принаймні після технічного обслуговування індексу) будуть фізично та логічно організовані таким чином, що потрібно набагато менше сторінок даних для задоволення запити. Це означає менше фізичного вводу-виводу, менше логічного зчитування, менше суперечок між Орендарями на одних і тих же сторінках даних, менше витраченого місця в буферному пулі (отже, покращена тривалість життя сторінки) тощо.
Для досягнення цієї покращеної продуктивності є дві основні витрати. Перше не так складно: потрібно регулярно підтримувати індекс, щоб протидіяти посиленій фрагментації. Друга - трохи менш весела.
Щоб протидіяти збільшенню проблем з обнюхуванням параметрів, вам потрібно розділити плани виконання між Орендарями. Спрощений підхід полягає у використанні WITH RECOMPILE
для проектів або OPTION (RECOMPILE)
підказки запиту, але це хіт на продуктивність, який може знищити всі отримані прибутки, поставивши TenantID
першими. Метод, який я знайшов найкращим, - це використовувати параметризований Dynamic SQL через sp_executesql
. Причина необхідності Dynamic 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, який буде відповідати обсягу даних саме цього Орендатора. Якщо той самий Орендар A виконає збережену процедуру ще раз для іншого, @OrderID
він повторно використає цей кешований план запитів. Інший Орендар, який виконує ту саму Збережену процедуру, генерував би текст запиту, який відрізнявся лише за значенням TenantID, але будь-якої різниці в тексті запиту достатньо для створення іншого плану. І план, згенерований для Орендаря B, не тільки відповідатиме обсягу даних для Орендаря В, але він також буде повторно використаний для Орендаря В для різних значень @OrderID
(оскільки цей предикат все ще параметризований).
Недоліками цього підходу є:
- Це трохи більше роботи, ніж просто введення простого запиту (але не всі запити повинні бути Dynamic SQL, лише ті, у кого виникають проблеми з нюханням параметрів).
- Залежно від кількості орендарів у системі, це збільшує розмір кешу плану, оскільки зараз для кожного запиту потрібен 1 план на кожний TenantID, який його викликає. Це може не бути проблемою, але це хоча б щось, що слід пам’ятати.
Dynamic SQL розбиває ланцюг власності, що означає, що доступ для читання / запису до таблиць неможливо припустити, маючи EXECUTE
дозвіл на процедуру зберігання. Просте, але менш безпечне виправлення - це просто надати користувачеві прямий доступ до таблиць. Це, звичайно, не ідеально, але це, як правило, компроміс швидко і легко. Більш безпечний підхід - використовувати захист на основі сертифікатів. Значить, створіть сертифікат, а потім створіть користувача з цього сертифіката, надайте цьому користувачеві потрібні дозволи (користувач, що базується на сертифікаті, або авторизація не може самостійно підключитися до сервера SQL), а потім підпишіть збережені процедури, що використовують динамічний SQL з цим той самий сертифікат через ДОПОМОГА ПІДПИС .
Більш детальну інформацію про підписання модуля та сертифікати див. У розділі: ModuleSigning.Info