Проєктування й експлуатація
Як спроєктувати схему, що переживе роки зростання, і як експлуатувати базу під навантаженням: нормальні форми й ключі, завантаження та звіти, партиціонування і зміна схеми на живій системі. На рівні Senior це розмова про компроміси й ризики, а не про синтаксис.
Перед цією темою: Індекси й плани запитів, Транзакції та блокування.
Що таке нормалізація і коли виправдана денормалізація?
Section titled “Що таке нормалізація і коли виправдана денормалізація?”Що це
Нормалізація — проєктування таблиць так, щоб один факт зберігався в одному місці. Назва магазину лежить у таблиці Store, а продаж посилається на неї за StoreId.
Нормальні форми
- 1НФ. У клітинці одне значення, немає груп, що повторюються: не стовпці
Phone1,Phone2,Phone3, а окрема таблиця телефонів. - 2НФ. За складеного ключа стовпець залежить від усього ключа. У рядку замовлення
(OrderId, ProductId)назва товару залежить лише відProductId— їй місце вProduct. - 3НФ. Неключовий стовпець не залежить від іншого неключового. Адреса магазину залежить від магазину, а не від продажу.
Типова помилка
Зберігати список через кому в одному стовпці: 'A12,B7,C3'. Такий список не проіндексувати, а пошук перетворюється на LIKE '%B7%'.
Як влаштовано
Нормальні форми прибирають три аномалії:
- вставлення — не можна завести магазин, доки в нього немає продажів;
- оновлення — адресу магазину треба змінювати в тисячах рядків, і частина копій лишиться старою;
- видалення — видалили останній продаж і втратили дані про магазин.
| Форма | Вимога |
|---|---|
| 1НФ | атомарні значення, немає груп, що повторюються |
| 2НФ | неключові стовпці залежать від усього складеного ключа |
| 3НФ | неключові стовпці не залежать один від одного |
| BCNF | будь-який детермінант — потенційний ключ |
Коли що обирати
- Транзакційна база — 3НФ. Запис дешевий, дані несуперечливі, читання пришвидшують індекси.
- Денормалізація — коли виміряно, що з’єднання чи агрегати на гарячому шляху дорогі: підсумок у шапці чека, залишок товару, звітна таблиця.
Пастки
- Історія — не дубль. Ціна в рядку чека — ціна на момент продажу, окремий факт. Посилання на поточну ціну з довідника спотворило б старі чеки.
- Копія без власника. Денормалізований стовпець змінюють з кількох місць, і він розходиться з джерелом.
Правило
Спершу 3НФ, денормалізація — за вимірюваннями та з явним механізмом синхронізації.
Під капотом
- Функціональна залежність
X → Y: значенняXоднозначно визначаєY. 3НФ допускає залежність, якщоYвходить до потенційного ключа; BCNF забороняє і її. - Розбіжність 3НФ і BCNF буває лише за кількох складених ключів, що перетинаються, тому на практиці таблиця в 3НФ майже завжди вже в BCNF.
Ціна денормалізації
| Спосіб | Хто синхронізує | Ціна |
|---|---|---|
Обчислюваний стовпець PERSISTED |
рушій | лише вирази за своїм рядком |
| Індексоване подання | рушій, у тій самій транзакції | дорожчий запис у базові таблиці, багато обмежень |
| Стовпець-агрегат: підсумок, залишок | код або тригер | гарячий рядок, ризик розбіжності |
| Звітна таблиця | завантаження за розкладом | дані відстають |
Компроміси
- Денормалізація переносить вартість з читання на запис. Залишок товару в одному рядку стає точкою конкуренції: усі продажі товару оновлюють його.
- Звітні бази денормалізують навмисно: їх пише одне завантаження, а читають багато запитів.
- Перебір теж шкодить. Модель EAV («сутність — атрибут — значення») заради гнучкості втрачає типи й обмеження, а кожен атрибут перетворюється на з’єднання.
Як перевірити в продакшені
- Регулярне звіряння: агрегат проти перерахунку з вихідних рядків.
- Гарячий рядок-агрегат видно за очікуваннями блокувань
LCK_M_UіLCK_M_Xна одному ключі.
Підсумок: нормалізують заради дешевого й несуперечливого запису; денормалізують точково, коли зрозуміло, хто і як синхронізує копію.
-- До: назва й адреса магазину повторюються в кожному продажу (порушення 3НФ)CREATE TABLE dbo.SaleFlat ( SaleId bigint NOT NULL PRIMARY KEY, StoreId int NOT NULL, StoreName nvarchar(100) NOT NULL, StoreAddress nvarchar(200) NOT NULL, Amount decimal(12,2) NOT NULL);
-- Після: факт про магазин зберігається один разCREATE TABLE dbo.Store ( StoreId int NOT NULL PRIMARY KEY, Name nvarchar(100) NOT NULL, Address nvarchar(200) NOT NULL);
CREATE TABLE dbo.Sale ( SaleId bigint NOT NULL PRIMARY KEY, StoreId int NOT NULL REFERENCES dbo.Store (StoreId), SoldAt datetime2(0) NOT NULL, Amount decimal(12,2) NOT NULL);
-- Переїзд магазину: один рядок замість тисячUPDATE dbo.Store SET Address = N'New address' WHERE StoreId = 42;- Чим 3НФ відрізняється від BCNF? — BCNF вимагає, щоб будь-який детермінант був потенційним ключем; 3НФ допускає залежність, якщо залежний стовпець входить до ключа.
- Чи порушує нормалізацію ціна в рядку чека? — Ні, це ціна на момент продажу, окремий історичний факт.
- Як денормалізувати, не втрачаючи узгодженості? — Віддати синхронізацію рушію (обчислюваний стовпець, індексоване подання); інакше — один власник копії та звіряння.
- Що таке EAV і чому його уникають? — «Сутність — атрибут — значення»: гнучко, але без типів і обмежень, зі з’єднанням на кожен атрибут.
- Чому звітні бази денормалізовані? — Їх пише одне завантаження, а читають багато запитів; менше з’єднань — швидші агрегати.
Який первинний ключ обрати: identity, GUID чи природний ключ?
Section titled “Який первинний ключ обрати: identity, GUID чи природний ключ?”Що це
| Вид ключа | Приклад |
|---|---|
| Сурогатний числовий | int або bigint IDENTITY |
| Сурогатний GUID | uniqueidentifier |
| Природний | код товару, податковий номер, код магазину |
Як користуватися
- За замовчуванням —
bigint IDENTITY(1, 1): компактний, зростає по порядку, не змінюється. - Природний ключ лишається стовпцем з обмеженням
UNIQUE: дублікати заборонені, але зовнішні ключі посилаються на сурогатний. - GUID — коли ідентифікатор потрібен до вставлення: його створює застосунок або інша база.
Типова помилка
Зробити первинним ключем штрихкод або податковий номер. Штрихкоди перевикористовують, податковий номер вводять з помилкою, і змінювати доводиться ключ в усіх таблицях, що на нього посилаються.
Як влаштовано
- Первинний ключ за замовчуванням створює кластерний індекс: рядки таблиці зберігаються впорядкованими за ключем.
- Ключ кластерного індексу слугує вказівником на рядок у кожному некластерному індексі. GUID на 16 байтів замість
bigintна 8 збільшує всі індекси таблиці. - Зростаючий ключ дописує рядки в останню сторінку. Випадковий
NEWID()вставляє в середину: сторінка ділиться навпіл, індекс фрагментується й заповнений наполовину.
Коли що обирати
| Ключ | Коли |
|---|---|
bigint IDENTITY |
за замовчуванням, одна база |
SEQUENCE |
номер потрібен до вставлення або спільний для кількох таблиць |
| Послідовний GUID | id створює застосунок, дані зливаються з кількох баз |
NEWID() |
майже ніколи як кластерний ключ |
Пастки
- Пропуски в
IDENTITY. Відкат транзакції або перезапуск сервера залишають пропуски. Номер чека без пропусків так не отримати. NEWSEQUENTIALID()працює лише вDEFAULT, а після перезапуску Windows може почати з меншого значення.- UUIDv7.
Guid.CreateVersion7()упорядкований за часом для .NET, але не дляuniqueidentifierу SQL Server.
Правило
bigint IDENTITY за замовчуванням; GUID — коли id створює застосунок, і тоді послідовний у порядку SQL Server.
Під капотом
- SQL Server порівнює
uniqueidentifierне зліва направо: старшими вважаються останні 6 байтів. В UUIDv7 час лежить у перших 6 байтах, тому порядок вставлення в індекс майже випадковий. NEWSEQUENTIALID()іSequentialGuidValueGenerator(генератор EF Core за замовчуванням для SQL Server) переставляють байти під цей порядок.- UUIDv7 зберігає порядок у
binary(16), записаному в big-endian:ToByteArray(bigEndian: true)(.NET 8+).binaryпорівнюється побайтово.
Ціна
- Зростаючий ключ на гарячій таблиці впирається в конкуренцію за останню сторінку (
PAGELATCH_EX).OPTIMIZE_FOR_SEQUENTIAL_KEY(SQL Server 2019) її згладжує. - GUID — 16 байтів у кожному індексі та зовнішньому ключі: на сотнях мільйонів рядків це гігабайти буферного пулу.
- Послідовні id легко вгадати: публічний номер замовлення відокремлюють від внутрішнього ключа.
Компроміси
- Природний ключ економить з’єднання, але його зміна каскадом іде всіма посиланнями. Виняток — короткі незмінні коди на кшталт ISO-коду валюти.
- Первинний ключ не зобов’язаний бути кластерним: GUID-ключ роблять некластерним, а кластеризують за зростаючим стовпцем.
IDENTITYвідомий лише після вставлення, тому пакетне вставлення пов’язаних рядків потребуєOUTPUTабоSEQUENCE.
Як перевірити в продакшені
sys.dm_db_index_physical_stats показує фрагментацію і заповненість сторінок (avg_page_space_used_in_percent), а очікування PAGELATCH_EX на одній сторінці — конкуренцію за кінець індексу.
Підсумок: ключ обирають за розміром, порядком вставлення й місцем генерації; «послідовний» GUID має бути послідовним у порядку порівняння SQL Server.
CREATE TABLE dbo.Product ( -- Сурогатний ключ: 8 байтів, зростає по порядку, не змінюється ProductId bigint IDENTITY(1, 1) NOT NULL CONSTRAINT PK_Product PRIMARY KEY CLUSTERED, -- Природний ключ: унікальний, але зовнішні ключі на нього не посилаються Sku varchar(20) NOT NULL CONSTRAINT UQ_Product_Sku UNIQUE, Name nvarchar(200) NOT NULL);
CREATE TABLE dbo.Receipt ( -- GUID, послідовний у порядку порівняння SQL Server ReceiptId uniqueidentifier NOT NULL CONSTRAINT DF_Receipt_Id DEFAULT NEWSEQUENTIALID() CONSTRAINT PK_Receipt PRIMARY KEY CLUSTERED, StoreId int NOT NULL, CreatedAt datetime2(0) NOT NULL);
-- Гаряче вставлення в кінець індексу (SQL Server 2019+)ALTER INDEX PK_Product ON dbo.Product SET (OPTIMIZE_FOR_SEQUENTIAL_KEY = ON);using System.Data.SqlTypes;
var ids = new List<Guid>();for (var i = 0; i < 5; i++){ ids.Add(Guid.CreateVersion7()); // .NET 9+: час у мс + випадкові біти Thread.Sleep(2); // у межах однієї мс порядок не гарантований}
Console.WriteLine(ids[0].Version); // 7Console.WriteLine(ids.SequenceEqual(ids.Order())); // True: у .NET порядок за часом
// SqlGuid порівнює як SQL Server: старші — останні 6 байтів, а там випадкові бітиvar asSqlServer = ids.OrderBy(id => new SqlGuid(id));Console.WriteLine(ids.SequenceEqual(asSqlServer)); // False (майже завжди)
// binary(16) порівнюється побайтово: big-endian зберігає порядок за часомvar asBinary = ids.OrderBy(id => Convert.ToHexString(id.ToByteArray(bigEndian: true)), StringComparer.Ordinal);Console.WriteLine(ids.SequenceEqual(asBinary)); // TrueSqlGuid з System.Data.SqlTypes повторює правила порівняння SQL Server, тому показує, як UUIDv7 ляже в індекс за uniqueidentifier.
- Чому випадковий GUID поганий для кластерного ключа? — Вставлення в середину індексу: розділення сторінок, фрагментація, напівпорожні сторінки, 16 байтів у кожному індексі.
- Чому в
IDENTITYбувають пропуски? — Значення не повертаються під час відкату й губляться з кешу під час перезапуску сервера. - Чи підходить
Guid.CreateVersion7()для SQL Server? — Дляuniqueidentifierпорядок втрачається; потрібенNEWSEQUENTIALID(), генератор EF Core або зберігання вbinary(16). - Чи зобов’язаний первинний ключ бути кластерним? — Ні: його можна зробити некластерним, а кластерний індекс побудувати за іншим стовпцем.
- Коли природний ключ доречний як первинний? — Коли він короткий і ніколи не змінюється: ISO-код валюти, код країни.
Як влаштований ETL і як будують завантаження в SSIS?
Section titled “Як влаштований ETL і як будують завантаження в SSIS?”Що це
ETL — три кроки:
- Extract — прочитати дані з джерела: бази, файлу, API.
- Transform — очистити й привести до формату цілі: типи, довідники, дублікати.
- Load — записати в цільову базу.
Як влаштований пакет SSIS
- Control flow — задачі та порядок їх виконання: «очистити staging», «завантажити дані», «виконати процедуру».
- Data flow — задача, через яку йдуть рядки: джерело → перетворення → приймач.
Як користуватися
Пакет розробляють у Visual Studio, розгортають на сервер у каталог SSISDB і запускають за розкладом завданням SQL Server Agent.
Типова помилка
Щоразу перезавантажувати всю таблицю. Поки даних мало, це працює, а через рік завантаження перестає вміщатися в нічне вікно.
Як влаштовано
Інкрементальне завантаження покроково:
- Прочитати з керувальної таблиці останній водяний знак.
- Data flow забирає рядки, новіші за нього, і швидким вставленням кладе у staging.
- Процедура переносить staging у ціль через
MERGEабоUPDATEіINSERT. - У тій самій транзакції зберегти новий водяний знак.
Коли що обирати
| Спосіб | Плюс | Мінус |
|---|---|---|
| Водяний знак | просто, без налаштування джерела | не бачить видалень |
| Change tracking | дешево, бачить видалення | лише ключі, без старих значень |
| CDC | значення до і після з журналу | SQL Server Agent, навантаження на журнал |
Пастки
- Порядкова обробка. OLE DB Command і Slowly Changing Dimension виконують запит на кожен рядок. Великі обсяги обробляють у staging одним запитом.
- Блокувальні перетворення. Sort і Aggregate чекають на весь потік і тримають його в пам’яті. Сортування віддають джерелу (
ORDER BYтаIsSortedна виході). - Дата зміни як водяний знак. Час ставиться до коміту, і рядки довгої транзакції проскакують.
Правило
Запуск безпечний для повтору: staging очищується на початку, перенесення йде за бізнес-ключем, водяний знак рухається лише після успіху.
Під капотом
- Data flow працює буферами в пам’яті. Синхронні перетворення (Derived Column, Lookup) змінюють рядки в тому самому буфері, асинхронні копіюють їх у нові.
- OLE DB Destination у режимі fast load вставляє пакетами;
Maximum insert commit sizeзадає розмір транзакції. - Checkpoints перезапускають пакет з упалої задачі control flow, але не з середини data flow, а цикли Foreach виконуються заново. Ідемпотентні кроки надійніші.
Компроміси
- Change tracking синхронний: кожен запис платить за облік. Якщо остання синхронізація старша за
CHANGE_TRACKING_MIN_VALID_VERSION, потрібне повне перезавантаження. - CDC асинхронний, але журнал не усікається, доки завдання захоплення його не обробило.
- У SSIS 2025 вилучено CDC-компоненти Attunity та Microsoft Connector for Oracle, а 32-бітний режим оголошено застарілим. Для нових завантажень Microsoft пропонує Azure Data Factory і Data Factory у Fabric; готові пакети запускають в ADF на Azure-SSIS Integration Runtime.
- Завантаження на .NET (
SqlBulkCopyу staging, процедура зMERGE) простіше тестувати й версіонувати, але логування, перезапуск і паралелізм пишуться вручну.
Як перевірити в продакшені
- Каталог SSISDB зберігає історію запусків:
catalog.executionsіcatalog.event_messages, а з рівнем логування Performance — час компонентів уcatalog.execution_component_phases. - Після завантаження звіряють кількість рядків і суми джерела та цілі.
Підсумок: надійний ETL — це інкремент без втрат, ідемпотентне перенесення і спостережуваність; інструмент другорядний.
SET XACT_ABORT ON;
DECLARE @from binary(8) = (SELECT LastVersion FROM etl.Watermark WHERE TableName = N'Sale');-- Верхня межа: рядки незавершених транзакцій дочекаються наступного запускуDECLARE @to binary(8) = MIN_ACTIVE_ROWVERSION();
-- Extract: у SSIS це data flow з джерела у stagingTRUNCATE TABLE stg.Sale;INSERT INTO stg.Sale (SaleId, StoreId, Amount)SELECT SaleId, StoreId, AmountFROM src.SaleWHERE RowVer >= @from AND RowVer < @to;
-- Load: повторний запуск не створить дублікатівBEGIN TRAN;
MERGE dw.Sale AS tUSING stg.Sale AS s ON t.SaleId = s.SaleIdWHEN MATCHED THEN UPDATE SET t.StoreId = s.StoreId, t.Amount = s.AmountWHEN NOT MATCHED THEN INSERT (SaleId, StoreId, Amount) VALUES (s.SaleId, s.StoreId, s.Amount);
-- Водяний знак рухається разом з данимиUPDATE etl.Watermark SET LastVersion = @to WHERE TableName = N'Sale';
COMMIT;Якщо взяти @@DBTS замість MIN_ACTIVE_ROWVERSION(), рядок із транзакції, яка закомітиться пізніше, отримає версію нижчу за збережену межу й не завантажиться ніколи.
- Чим change tracking відрізняється від CDC? — CT синхронно зберігає ключі змінених рядків і версію, CDC асинхронно читає журнал і зберігає значення до і після.
- Чому
@@DBTSне можна брати як водяний знак? — Рядки незавершених транзакцій закомітяться з версією нижчою за межу й будуть пропущені. - Як перезапустити пакет SSIS, що впав? — Checkpoints з упалої задачі control flow; надійніші ідемпотентні кроки, які можна просто запустити заново.
- Як задати різні рядки підключення для тесту й продакшену? — Параметри проєкту та середовища (environments) у каталозі SSISDB.
- Як пришвидшити data flow? — Прибрати порядкові й блокувальні перетворення, fast load у staging, сортування та з’єднання віддати базі.
Як побудувати звітність, щоб звіти не поклали робочу базу?
Section titled “Як побудувати звітність, щоб звіти не поклали робочу базу?”Що це
| OLTP | OLAP | |
|---|---|---|
| Запити | короткі, за ключем | агрегати за мільйонами рядків |
| Приклад | продаж на касі | виторг за магазинами за рік |
| Схема | нормалізована | денормалізована, «зірка» |
Навіщо розділяти
Важкий звіт займає процесор і диск робочої бази та може блокувати запис. Каси й сайт починають чекати.
Як користуватися
- Звіти будують у SSRS: звіт описаний у файлі RDL, сервер звітів виконує запит і віддає результат у браузер, Excel або PDF.
- Важкі звіти підключають не до робочої бази, а до її копії або до сховища даних.
Типова помилка
Лікувати блокування звітів підказкою NOLOCK. Звіт починає читати незакомічені дані й може пропустити або двічі порахувати рядки.
Як влаштовано
Звіт заважає робочій базі трьома шляхами:
- Ресурси. Скани й сортування займають CPU, диск і пам’ять під запити.
- Кеш. Прочитані сторінки витісняють з буферного пулу гарячі дані.
- Блокування. Під READ COMMITTED читання ставить спільні блокування й конфліктує із записом.
Коли що обирати
| Рішення | Затримка даних | Коли |
|---|---|---|
| RCSI на робочій базі | немає | звіти легкі, заважають лише блокування |
| Кеш і знімки SSRS | хвилини або доба | один звіт відкривають багато людей |
| Репліка Always On | секунди | оперативні звіти, Enterprise |
| Вітрина даних | до наступного ETL | аналітика, історія, важкі агрегати |
Пастки
- Репліка — та сама схема. Індекс під звіт створюють на первинній базі, і він уповільнює запис. Власні індекси можливі лише у вітрині або в базі-передплатнику реплікації.
- Параметри без меж. Період «за весь час» за замовчуванням перетворює звіт на повний скан.
- Підписки в один час. Сотні розсилок о 9:00 — пік навантаження, який легко розмазати.
Правило
Спершу знайти важкі звіти, потім відводити їх за допустимою затримкою: оперативні — на репліку, аналітику — у вітрину.
Під капотом
- Репліка для читання застосовує журнал потоком redo. Усі запити на ній ідуть під snapshot isolation, підказки блокувань ігноруються.
- Платить первинна база: за наявності репліки для читання до змінених рядків додаються 14 байтів під версії, що може спричинити розділення сторінок.
- Довгий звіт на репліці тримає Sch-S і може затримати redo DDL та очищення ghost-записів на первинній.
- Columnstore зберігає дані за стовпцями зі стисненням і обробляє пакетами рядків. Із SQL Server 2016 некластерний columnstore на робочій таблиці оновлюваний — це real-time operational analytics.
Компроміси
| Варіант | Ціна |
|---|---|
| Репліка Always On | Enterprise; схема й індекси як у первинної; лаг росте під навантаженням |
| Columnstore на робочій таблиці | дорожчий кожен запис, місце під індекс |
| Вітрина із зіркою | ETL, затримка, ще одна модель даних |
Basic availability group у Standard репліки для читання не дає.
Звітний сервер у 2025+
SSRS 2022 — остання версія, підтримка до 11 січня 2033 року. Із SQL Server 2025 локальна звітність — Power BI Report Server: ті самі RDL-звіти плюс інтерактивні PBIX, право на нього входить у ліцензії Enterprise і Standard. Хмарний шлях — Power BI, де RDL працюють як paginated reports.
Як перевірити в продакшені
ExecutionLog3у базі сервера звітів показує для кожного запуску час отримання даних, обробки й рендерингу.- Відставання репліки —
sys.dm_hadr_database_replica_states:log_send_queue_size,redo_queue_size.
Підсумок: звітність розводять із транзакціями за допустимою затримкою, а платформу обирають з урахуванням кінця лінійки SSRS.
-- База сервера звітів: за замовчуванням ReportServer, журнал зберігається 60 днівUSE ReportServer;
-- Які звіти найдовше чекали на базу за тижденьSELECT TOP (10) ItemPath, COUNT(*) AS Runs, SUM(CAST(TimeDataRetrieval AS bigint)) / 1000 AS DataSecTotal, AVG(CAST(TimeDataRetrieval AS bigint)) / 1000 AS DataSecAvg, MAX([RowCount]) AS MaxRowsFROM dbo.ExecutionLog3WHERE TimeStart >= DATEADD(DAY, -7, SYSDATETIME())GROUP BY ItemPathORDER BY DataSecTotal DESC;-- Оперативна аналітика просто по робочій таблиці (SQL Server 2016+):-- транзакції пишуть у звичайний індекс, агрегати читають стиснені стовпціCREATE NONCLUSTERED COLUMNSTORE INDEX NCCI_Sale ON dbo.Sale (StoreId, SoldAt, Amount);
SELECT StoreId, SUM(Amount) AS RevenueFROM dbo.SaleWHERE SoldAt >= '2026-10-01'GROUP BY StoreId;- Чим схема «зірка» відрізняється від робочої схеми? — Таблиця фактів і денормалізовані виміри під агрегати проти нормалізованих таблиць під запис.
- Чому звіт на репліці не блокує запис? — На репліці для читання всі запити йдуть під snapshot isolation, підказки блокувань ігноруються.
- Чи можна створити індекс лише на репліці Always On? — Ні, схема спільна; індекс створюють на первинній або використовують вітрину чи реплікацію.
- Що буде з SSRS після SQL Server 2025? — Нових версій немає, SSRS 2022 підтримується до 2033 року, заміна — Power BI Report Server.
- Як знайти звіт, який навантажує базу? —
ExecutionLog3у базі сервера звітів: час отримання даних і кількість запусків.
Як масштабувати базу: партиціонування, репліки, шардування й архів?
Section titled “Як масштабувати базу: партиціонування, репліки, шардування й архів?”Що це
- Партиціонування — таблиця зберігається секціями за значенням стовпця, наприклад за місяцем продажу. Для запитів це одна таблиця.
- Репліка для читання — копія бази, на яку відводять звіти.
- Шардування — дані різних магазинів або клієнтів лежать у різних базах на різних серверах.
- Архівування — старі дані переносять з робочої таблиці в архів.
Навіщо
Велику таблицю довше обслуговувати, копіювати й чистити. Видалити минулий рік через DELETE — години блокувань і гігабайти журналу, а з партиціонуванням це перемикання секції.
Типова помилка
Чекати, що партиціонування саме пришвидшить запити. Якщо в умові немає стовпця секціонування, запит читає всі секції й може стати повільнішим.
Як влаштоване партиціонування
- Функція секціонування задає межі:
RANGE RIGHT FOR VALUES ('2026-09-01', '2026-10-01'). - Схема секціонування розкладає секції по файлових групах.
- Таблицю й індекси створюють на схемі. Індекс на тій самій схемі — вирівняний;
SWITCHпрацює лише з вирівняними індексами.
Ковзне вікно для архіву
SPLIT RANGEдодає порожню секцію під наступний місяць.SWITCH PARTITIONпереносить найстаріший місяць в архівну таблицю тієї самої структури.MERGE RANGEприбирає межу, що спорожніла.
Коли що обирати
| Проблема | Рішення |
|---|---|
| Таблицю довго обслуговувати й чистити | партиціонування |
| Читання перевантажує сервер | репліки для читання |
| Запис не вміщається в один сервер | шардування |
Пастки
- Унікальний індекс містить стовпець секціонування, інакше він не вирівняний. Звідси складений ключ
(SaleId, SoldOn). - Запит без ключа секціонування шукає в кожній секції. Microsoft прямо пише, що в OLTP партиціонування рідко пришвидшує запити.
SPLITіMERGEнепорожньої секції переміщують рядки й пишуть журнал, тому крайні секції тримають порожніми.
Під капотом
SWITCHзмінює належність сторінок, дані не копіюються. Умови: однакові стовпці й індекси, та сама файлова група, ціль порожня; під час завантаження в секцію джерело підтверджує діапазон CHECK-обмеженням.SWITCHпотрібне монопольне блокування обох таблиць. Із SQL Server 2014WAIT_AT_LOW_PRIORITYне дає йому стати в чергу перед усіма запитами.TRUNCATE TABLE ... WITH (PARTITIONS (2))(SQL Server 2016+) очищує секцію без архіву. Партиціонування є в усіх редакціях із SQL Server 2016 SP1.
Ціна
- До 15 000 секцій, але тисячі секцій коштують пам’яті, а запити без виключення секцій сповільнюються.
- Репліки для читання — лише Enterprise; basic availability group у Standard не дає читати з вторинної.
Компроміси шардування
| Що втрачаємо | Чим платимо |
|---|---|
| З’єднання й агрегати між шардами | запити до всіх шардів, звіти через сховище |
| Транзакції між шардами | саги, outbox, ідемпотентність |
| Глобальна унікальність | GUID або ключ із номером шарда |
| Рівномірність | перебалансування, «гарячі» шарди |
Довідники на кшталт товарів і магазинів копіюють у кожен шард.
Як перевірити в продакшені
sys.dm_db_partition_statsпоказує рядки за секціями, а план —Actual Partition Count.- Відставання реплік —
sys.dm_hadr_database_replica_states.
Підсумок: один сервер вичавлюють до кінця, партиціонують заради обслуговування, а шардують, лише коли запис уперся в сервер і є природний ключ розділу.
-- RANGE RIGHT: межа — перше число місяця; крайні секції тримаємо порожнімиCREATE PARTITION FUNCTION pfMonth (date) AS RANGE RIGHT FOR VALUES ('2026-08-01', '2026-09-01', '2026-10-01', '2026-11-01');CREATE PARTITION SCHEME psMonth AS PARTITION pfMonth ALL TO ([PRIMARY]);
CREATE TABLE dbo.Sale ( SaleId bigint NOT NULL, SoldOn date NOT NULL, Amount decimal(12,2) NOT NULL, CONSTRAINT PK_Sale PRIMARY KEY (SaleId, SoldOn) -- ключ містить стовпець секціонування) ON psMonth (SoldOn);
-- Архів: та сама структура й файлова група, порожнійCREATE TABLE dbo.SaleArchive ( SaleId bigint NOT NULL, SoldOn date NOT NULL, Amount decimal(12,2) NOT NULL, CONSTRAINT PK_SaleArchive PRIMARY KEY (SaleId, SoldOn)) ON [PRIMARY];
-- 1. Порожня секція під грудень, поки даних за листопад ще немаєALTER PARTITION SCHEME psMonth NEXT USED [PRIMARY];ALTER PARTITION FUNCTION pfMonth() SPLIT RANGE ('2026-12-01');
-- 2. Серпень (секція 2) іде в архів: змінюються лише метаданіALTER TABLE dbo.Sale SWITCH PARTITION 2 TO dbo.SaleArchive WITH (WAIT_AT_LOW_PRIORITY (MAX_DURATION = 1 MINUTES, ABORT_AFTER_WAIT = SELF));
-- 3. Прибрати межу між двома порожніми секціямиALTER PARTITION FUNCTION pfMonth() MERGE RANGE ('2026-08-01');- Чи пришвидшує партиціонування запити? — Лише за виключення секцій за ключем секціонування; інакше часто сповільнює.
- Чому
SWITCHвиконується миттєво? — Змінюються метадані належності сторінок, рядки не копіюються. - Навіщо тримати крайні секції порожніми? — Тоді
SPLITіMERGEне переміщують рядки й не роздувають журнал. - Чим шардування відрізняється від партиціонування? — Секції живуть в одній базі на одному сервері, шарди — у різних базах на різних серверах.
- Як обрати ключ шардування? — За межею, всередині якої живе майже кожна транзакція, і з рівномірним навантаженням: магазин, регіон, клієнт.
Як змінювати схему бази без простою?
Section titled “Як змінювати схему бази без простою?”Що це
Зміна схеми без простою — база змінюється, поки застосунок працює й магазини продовжують продавати.
Як користуватися
Перейменування стовпця Phone на PhoneNumber покроково:
- Додати стовпець
PhoneNumber, дозволившиNULL. Старий застосунок його не помічає. - Випустити версію застосунку, яка пише в обидва стовпці.
- Скопіювати старі дані невеликими пакетами.
- Випустити версію, яка читає лише
PhoneNumber. - Видалити
Phone, коли старих версій застосунку не лишилося.
Типова помилка
Перейменувати або видалити стовпець в одному релізі з кодом. Поки оновлено не всі сервери, стара версія звертається до стовпця, якого вже немає, і падає.
Як влаштовано
Expand/contract — три фази:
- Expand. Лише сумісні зміни: новий nullable-стовпець, таблиця, індекс. Старий код працює.
- Migrate. Код пише в старе й нове, backfill переносить історію, читання перемикається на нове.
- Contract. Коли старого коду немає і вікно відкату минуло, старе видаляють.
Скільки тримається блокування
| Операція | Як виконується |
|---|---|
| Новий nullable-стовпець | метадані, миттєво |
| NOT NULL з константним default | метадані в Enterprise (2012+), інакше перезапис рядків |
Зміна типу, наприклад int → bigint |
перезапис усіх рядків під Sch-M |
Індекс з ONLINE = ON |
онлайн, короткі блокування на початку й наприкінці; Enterprise |
Пастки
- Черга за Sch-M.
ALTERчекає на довгий звіт, а всі нові запити до таблиці чекають наALTER. Перед DDL ставлятьSET LOCK_TIMEOUTі повторюють спробу. - Один великий
UPDATE. Backfill однією транзакцією роздуває журнал, спричиняє ескалацію блокувань до таблиці й затримує репліки. Потрібні пакети по кілька тисяч рядків. - Перейменування в один крок.
sp_renameстовпця, який читає робоча версія, — несумісна зміна.
Правило
Кожна міграція сумісна з поточною і попередньою версією застосунку.
Під капотом
- Sch-M несумісне з жодним блокуванням, навіть зі стабільністю схеми в читачів. Важливо, скільки DDL його тримає і скільки чекає на його отримання.
- Ескалація спрацьовує приблизно на 5 000 блокувань одного оператора на одному об’єкті. Пакети, менші за цей поріг, тримають порядкові блокування й не зупиняють інших.
- ADR (SQL Server 2019+) робить відкат довгої транзакції миттєвим і усікає журнал під час неї; без ADR скасування годинного
UPDATEможе тривати стільки ж.
Зміна типу на великій таблиці
- Додати стовпець нового типу, nullable.
- Синхронізувати нові зміни — кодом або тригером.
- Backfill пакетами за діапазонами ключа.
- Перемкнути читання й запис, потім видалити старий стовпець.
ALTER COLUMN ... WITH (ONLINE = ON) (SQL Server 2016+, Enterprise) лишає таблицю доступною, але рядки однаково переписуються.
Компроміси
- State-based розгортання (dacpac,
sqlpackage) будує скрипт порівнянням схем і за деяких змін перестворює таблицю копіюванням, тому скрипт читають до продакшену. Міграції (EF Core, DbUp) явні, але порядок кроків на вас. - Після expand відкочують лише код: схема сумісна зі старою версією. Зворотні міграції з утратою даних у продакшені замінюють виправленням уперед.
- Тригер синхронізації ловить усіх, хто пише, включно з ETL, але здорожує кожен запис.
Як перевірити в продакшені
- Під час backfill стежать за
log_reuse_wait_descуsys.databasesіredo_queue_sizeреплік; пауза між пакетами — регулятор. - Черга за DDL видна в
sys.dm_exec_requests:wait_typeLCK_M_SCH_Mіblocking_session_id.
Підсумок: без простою — це сумісність із двома версіями коду, короткі блокування й керована черга за Sch-M.
-- DDL чекає на блокування не довше 5 секунд і не тримає чергу за собоюSET LOCK_TIMEOUT 5000;
-- Expand: новий nullable-стовпець ширший за старий — лише метаданіALTER TABLE dbo.Receipt ADD TotalAmount decimal(14,2) NULL;
-- Backfill пакетами за діапазоном ключа: короткі транзакції без ескалаціїDECLARE @batch bigint = 4000, @from bigint = 0, @max bigint;SELECT @max = MAX(ReceiptId) FROM dbo.Receipt;
WHILE @from < @maxBEGIN UPDATE dbo.Receipt SET TotalAmount = Total WHERE ReceiptId > @from AND ReceiptId <= @from + @batch AND TotalAmount IS NULL; -- повторний запуск продовжить з місця зупинки
SET @from += @batch; WAITFOR DELAY '00:00:00.100'; -- місце для робочого навантаження й реплікEND;
-- Contract — окремим релізом, коли код більше не читає старий стовпець-- ALTER TABLE dbo.Receipt DROP COLUMN Total;Поки триває backfill, нова версія застосунку пише в обидва стовпці, тому нові чеки вже заповнені.
- Чому
ALTER TABLEна мілісекунди може покласти систему? — Він чекає на Sch-M за довгим запитом, а всі нові запити стають у чергу за ним. - Як додати NOT NULL-стовпець у таблицю на мільярд рядків? — В Enterprise з константним default це метадані; інакше nullable-стовпець, backfill пакетами, а фінальний
NOT NULLперевірить усі рядки. - Навіщо пакети по кілька тисяч рядків? — Короткі транзакції, немає ескалації до таблиці, журнал усікається, репліки не відстають.
- Як відкотити реліз зі зміною схеми? — Відкочують код: після expand схема сумісна зі старою версією; видалення йде окремим пізнім релізом.
- Чим dacpac відрізняється від міграцій? — Скрипт генерується порівнянням схем і може перестворити таблицю; міграції явні, але порядок кроків на вас.