Перейти к содержимому
16

Содержание курса "Продвинутый курс по MS SQL"

Продвинутый MS SQL · Обзорная статья

Продвинутый курс по Transact-SQL: восемь уроков от оконных функций до секционирования больших таблиц и финальный проект. Все упражнения выполняются на учебной базе TechStore в среде DataGrip.

База TechStore специально содержит дефекты: дубликаты клиентов, перекос данных по городам и отсутствующие индексы. Эти дефекты являются учебным материалом уроков.

Содержание

1. О курсе

Курс предназначен для разработчиков и администраторов баз данных, которые уверенно пишут запросы с соединениями и группировками и хотят перейти на уровень оптимизации и промышленной разработки. Формат каждого урока: конспект теории, практикум с заданиями, контрольные вопросы и чек-лист самопроверки.

УрокТемы
1. Оконные функции Рейтинг, смещение строк, крайние значения, агрегаты в окне, фреймы, острова активности
2. CTE и рекурсия Дедупликация, обход иерархии, агрегирование по дереву, обнаружение циклов
3. PIVOT, JSON и XML Статическое и динамическое свёртывание, чтение и изменение JSON, XML-параметры
4. Планы выполнения Чтение планов, статистика ввода-вывода, поисковые предикаты, дочитка, неявные преобразования
5. Индексы Покрывающие, фильтрованные и столбцовые индексы, фрагментация, неиспользуемые индексы
6. Программирование на T-SQL Процедуры с транзакциями, триггеры, функции, безопасный динамический SQL
7. Транзакции и конкурентность Уровни изоляции, потерянное обновление, взаимоблокировки, версионирование
8. Секционирование Функции и схемы секций, устранение секций, скользящее окно
9. Финальный проект Комплексная работа по всем темам с критериями приёмки
Трудоёмкость: на восемь уроков и финальный проект закладывается от 35 до 50 часов занятий.
↑ К оглавлению

2. Что понадобится

Необходимое окружение:

  • Microsoft SQL Server выпуска 2017 или новее; для урока о секционировании достаточно выпуска 2016 SP1 или новее, включая редакции Standard и Express. Рекомендуется бесплатная редакция Developer.
  • Среда DataGrip любой актуальной версии с подключением к серверу.
  • Права на создание базы данных на сервере.
  • Около двух минут на выполнение двух скриптов развёртывания.
Внимание: первый скрипт пересоздаёт базу TechStore. Если на сервере уже существует база с таким именем, её данные будут удалены без возможности восстановления. Выполняйте скрипт только на учебном сервере.
↑ К оглавлению

3. Подготовка базы TechStore

Порядок развёртывания:

  1. Откройте скрипт 1 в консоли запросов DataGrip и выполните его целиком: выделить всё и нажать Ctrl+Enter.
  2. Откройте скрипт 2 и выполните его целиком; длительность — около двух минут.
  3. Убедитесь, что контрольная сводка в конце скрипта 2 возвращает 25000 заказов.
Подключение в DataGrip: источник данных создаётся в диалоге File → New → Data Source → Microsoft SQL Server. Представление плана запроса вызывается через контекстное меню редактора: правый щелчок → Explain Plan.

Скрипт 1. Создание схемы

SET NOCOUNT ON;
USE master;
GO

IF DB_ID(N'TechStore') IS NOT NULL
BEGIN
    ALTER DATABASE TechStore SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
    DROP DATABASE TechStore;
END
GO

CREATE DATABASE TechStore;
GO

ALTER DATABASE TechStore SET RECOVERY SIMPLE;
GO

USE TechStore;
GO

CREATE TABLE dbo.Categories
(
    CategoryID       INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
    Name             NVARCHAR(100)     NOT NULL,
    ParentCategoryID INT               NULL
        REFERENCES dbo.Categories (CategoryID)
);

CREATE TABLE dbo.Products
(
    ProductID   INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
    SKU         NVARCHAR(20)      NOT NULL UNIQUE,
    Name        NVARCHAR(200)     NOT NULL,
    CategoryID  INT               NOT NULL REFERENCES dbo.Categories (CategoryID),
    Price       DECIMAL(18,2)     NOT NULL,
    StockQty    INT               NOT NULL CONSTRAINT DF_Products_Stock  DEFAULT (0),
    Attributes  NVARCHAR(MAX)     NULL,
    IsActive    BIT               NOT NULL CONSTRAINT DF_Products_Active DEFAULT (1)
);

CREATE TABLE dbo.Customers
(
    CustomerID   INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
    FullName     NVARCHAR(200)     NOT NULL,
    Email        VARCHAR(320)      NULL,
    City         NVARCHAR(100)     NOT NULL,
    Segment      NVARCHAR(20)      NOT NULL CONSTRAINT DF_Customers_Segment DEFAULT (N'Retail'),
    RegisteredAt DATETIME2(0)      NOT NULL CONSTRAINT DF_Customers_Reg     DEFAULT (SYSDATETIME())
);

CREATE TABLE dbo.Orders
(
    OrderID     INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
    CustomerID  INT               NOT NULL REFERENCES dbo.Customers (CustomerID),
    OrderDate   DATETIME2(0)      NOT NULL,
    Status      NVARCHAR(20)      NOT NULL CONSTRAINT DF_Orders_Status DEFAULT (N'New'),
    TotalAmount DECIMAL(18,2)     NULL
);

CREATE TABLE dbo.OrderItems
(
    OrderItemID INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
    OrderID     INT               NOT NULL REFERENCES dbo.Orders (OrderID),
    ProductID   INT               NOT NULL REFERENCES dbo.Products (ProductID),
    Quantity    INT               NOT NULL,
    UnitPrice   DECIMAL(18,2)     NOT NULL
);

CREATE TABLE dbo.Payments
(
    PaymentID INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
    OrderID   INT               NOT NULL REFERENCES dbo.Orders (OrderID),
    PaidAt    DATETIME2(0)      NOT NULL,
    Amount    DECIMAL(18,2)     NOT NULL,
    Method    NVARCHAR(20)      NOT NULL
);

CREATE TABLE dbo.PriceHistory
(
    HistoryID INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
    ProductID INT               NOT NULL REFERENCES dbo.Products (ProductID),
    OldPrice  DECIMAL(18,2)     NOT NULL,
    NewPrice  DECIMAL(18,2)     NOT NULL,
    ChangedAt DATETIME2(0)      NOT NULL CONSTRAINT DF_PriceHistory_ChangedAt DEFAULT (SYSDATETIME())
);

CREATE TABLE dbo.AuditLog
(
    AuditID   BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY,
    TableName SYSNAME      NOT NULL,
    Action    NVARCHAR(20) NOT NULL,
    Details   NVARCHAR(MAX) NULL,
    CreatedAt DATETIME2(0) NOT NULL CONSTRAINT DF_AuditLog_CreatedAt DEFAULT (SYSDATETIME())
);

CREATE TABLE dbo.Numbers
(
    N INT NOT NULL PRIMARY KEY
);
GO
SQL

Скрипт 2. Наполнение данными

USE TechStore;
GO
SET NOCOUNT ON;

WITH L0 AS (SELECT 1 AS c FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) t(c))
   , L1 AS (SELECT 1 AS c FROM L0 a CROSS JOIN L0 b)
   , L2 AS (SELECT 1 AS c FROM L1 a CROSS JOIN L1 b)
   , L3 AS (SELECT 1 AS c FROM L2 a CROSS JOIN L0 b)
   , Nums AS (SELECT TOP (100000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n FROM L3)
INSERT dbo.Numbers (N) SELECT n FROM Nums;

SET IDENTITY_INSERT dbo.Categories ON;
INSERT dbo.Categories (CategoryID, Name, ParentCategoryID) VALUES
 (1,  N'Электроника',         NULL),
 (2,  N'Дом и кухня',          NULL),
 (3,  N'Смартфоны',            1),
 (4,  N'Ноутбуки',             1),
 (5,  N'Аксессуары',           1),
 (6,  N'Кухонная техника',     2),
 (7,  N'Мебель',               2),
 (8,  N'Игровые ноутбуки',     4),
 (9,  N'Ультрабуки',           4),
 (10, N'Зарядные устройства',  5),
 (11, N'Чехлы',                5),
 (12, N'Складные смартфоны',   3);
SET IDENTITY_INSERT dbo.Categories OFF;

INSERT dbo.Products (SKU, Name, CategoryID, Price, StockQty, Attributes)
SELECT N'SKU-' + RIGHT(N'0000' + CAST(n.N AS NVARCHAR(5)), 5),
       N'Товар ' + CAST(n.N AS NVARCHAR(10)),
       CHOOSE(1 + n.N % 8, 8, 9, 10, 11, 12, 3, 6, 7),
       CAST(500 + ABS(CHECKSUM(NEWID())) % 500000 / 100.0 AS DECIMAL(18,2)),
       ABS(CHECKSUM(NEWID())) % 500,
       (SELECT brand           = CHOOSE(1 + n.N % 5, N'TechNoir', N'Voltex', N'Orion', N'Kappa', N'Delta'),
               color           = CHOOSE(1 + n.N % 4, N'black', N'white', N'gray', N'blue'),
               warranty_months = 6 * (1 + n.N % 4)
        FOR JSON PATH)
FROM dbo.Numbers n
WHERE n.N BETWEEN 1 AND 500;

UPDATE dbo.Products SET IsActive = 0     WHERE ProductID % 20 = 0;
UPDATE dbo.Products SET Attributes = NULL WHERE ProductID % 10 = 0;

INSERT dbo.Customers (FullName, Email, City, Segment, RegisteredAt)
SELECT N'Клиент ' + CAST(n.N AS NVARCHAR(10)),
       CASE
           WHEN n.N % 7  = 0 THEN NULL
           WHEN n.N % 11 = 0 THEN 'shared' + CAST((n.N / 11) % 5 + 1 AS VARCHAR(2)) + Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.'
           ELSE 'user' + CAST(n.N AS VARCHAR(10)) + Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.'
       END,
       CASE WHEN n.N % 10 BETWEEN 0 AND 5 THEN N'Москва'
            ELSE CHOOSE(1 + n.N % 8, N'Санкт-Петербург', N'Казань', N'Новосибирск',
                        N'Екатеринбург', N'Сочи', N'Самара', N'Казань', N'Воронеж')
       END,
       CHOOSE(1 + n.N % 10, N'VIP', N'Retail', N'Retail', N'Retail', N'Retail',
              N'Wholesale', N'Retail', N'Retail', N'Retail', N'VIP'),
       DATEADD(DAY, -ABS(CHECKSUM(NEWID()) % 1500), SYSDATETIME())
FROM dbo.Numbers n
WHERE n.N BETWEEN 1 AND 3000;

INSERT dbo.Orders (CustomerID, OrderDate)
SELECT 1 + ABS(CHECKSUM(NEWID()) % 3000),
       DATEADD(SECOND, -ABS(CHECKSUM(NEWID()) % 63072000), SYSDATETIME())
FROM dbo.Numbers n
WHERE n.N BETWEEN 1 AND 25000;

DECLARE @ProdCount INT = (SELECT COUNT(*) FROM dbo.Products);

WITH k AS (SELECT v FROM (VALUES (1),(2),(3),(4)) x(v))
INSERT dbo.OrderItems (OrderID, ProductID, Quantity, UnitPrice)
SELECT o.OrderID,
       p.ProductID,
       1 + (o.OrderID + k.v) % 5,
       p.Price
FROM dbo.Orders o
JOIN k           ON k.v BETWEEN 1 AND 1 + (o.OrderID % 4)
JOIN dbo.Products p
     ON p.ProductID = ((o.OrderID * 37 + k.v * 91) % @ProdCount) + 1;

UPDATE dbo.Orders
SET Status = CASE OrderID % 20
                WHEN 0 THEN N'Cancelled'
                WHEN 1 THEN N'New'
                WHEN 2 THEN N'New'
                ELSE CHOOSE(OrderID % 3 + 1, N'Paid', N'Shipped', N'Delivered')
             END;

INSERT dbo.Payments (OrderID, PaidAt, Amount, Method)
SELECT o.OrderID,
       DATEADD(MINUTE, 30 + o.OrderID % 600, o.OrderDate),
       s.Amt,
       CHOOSE(o.OrderID % 3 + 1, N'Card', N'Cash', N'Transfer')
FROM dbo.Orders o
JOIN (SELECT OrderID, SUM(Quantity * UnitPrice) AS Amt
      FROM dbo.OrderItems
      GROUP BY OrderID) s ON s.OrderID = o.OrderID
WHERE o.Status IN (N'Paid', N'Shipped', N'Delivered');

UPDATE o
SET TotalAmount = s.Amt
FROM dbo.Orders o
JOIN (SELECT OrderID, SUM(Quantity * UnitPrice) AS Amt
      FROM dbo.OrderItems
      GROUP BY OrderID) s ON s.OrderID = o.OrderID;

INSERT dbo.PriceHistory (ProductID, OldPrice, NewPrice, ChangedAt)
SELECT ProductID, Price * 1.15, Price * 1.05, DATEADD(DAY, -500 + ProductID % 100, SYSDATETIME())
FROM dbo.Products WHERE ProductID % 5 = 0
UNION ALL
SELECT ProductID, Price * 1.05, Price * 1.10, DATEADD(DAY, -300 + ProductID % 60, SYSDATETIME())
FROM dbo.Products WHERE ProductID % 7 = 0
UNION ALL
SELECT ProductID, Price * 1.10, Price,        DATEADD(DAY, -100 + ProductID % 40, SYSDATETIME())
FROM dbo.Products WHERE ProductID % 3 = 0;

CHECKPOINT;

SELECT 'Categories'  AS [table], COUNT(*) AS rows_cnt FROM dbo.Categories
UNION ALL SELECT 'Products',     COUNT(*) FROM dbo.Products
UNION ALL SELECT 'Customers',    COUNT(*) FROM dbo.Customers
UNION ALL SELECT 'Orders',       COUNT(*) FROM dbo.Orders
UNION ALL SELECT 'OrderItems',   COUNT(*) FROM dbo.OrderItems
UNION ALL SELECT 'Payments',     COUNT(*) FROM dbo.Payments
UNION ALL SELECT 'PriceHistory', COUNT(*) FROM dbo.PriceHistory
ORDER BY 1;
SQL
Проверка: после второго скрипта запрос SELECT COUNT(*) FROM TechStore.dbo.Orders обязан вернуть 25000.
↑ К оглавлению

4. Структура базы TechStore

ОбъектНазначение и учебные дефекты
Categories Иерархия категорий глубиной до трёх уровней; материал для рекурсии урока 2
Products Пятьсот товаров; JSON-атрибуты для урока 3; у пяти процентов товаров снят признак активности
Customers Три тысячи клиентов; намеренные дубликаты адресов электронной почты; столбец Email имеет тип VARCHAR; шестьдесят процентов клиентов из Москвы для задачи о дисбалансе данных
Orders Двадцать пять тысяч заказов за два года; денормализованное поле TotalAmount
OrderItems Около шестидесяти двух тысяч позиций заказов
Payments Оплаты для заказов в оплаченных статусах
PriceHistory Три волны изменения цен; пополняется триггером в уроке 6
AuditLog Универсальный журнал аудита для урока 6 и финального проекта
Numbers Служебная таблица чисел от 1 до 100000 для генерации последовательностей
↑ К оглавлению

5. Как работать с курсом

Правила работы с материалом:

  • Задачи практикума решаются самостоятельно до обращения к ответам; эталонное решение не является единственно верным.
  • Любая задача оптимизации засчитывается только при подтверждении планом запроса и статистикой чтения, а не одним временем выполнения.
  • Решения принимаются в мультимножественном стиле; курсоры применяются только там, где это прямо оговорено.
  • Для урока о транзакциях потребуются две консоли запросов DataGrip: каждая консоль является отдельным соединением.

Порядок прохождения каждого урока

Единый ритуал работы с материалом.

  1. Прочитать конспект разделов и выполнить примеры кода на своей базе.
  2. Выполнить задания практикума и зафиксировать результаты замеров.
  3. Ответить на контрольные вопросы вслух или письменно, не подглядывая.
  4. Сверить ответы со справочной статьёй и заполнить чек-лист самопроверки.
Замеры: шаблон замера во всех уроках: включить SET STATISTICS IO, TIME ON, выполнить запрос, выключить. Метрика logical reads не зависит от состояния дискового кэша и потому надёжнее времени.
↑ К оглавлению

6. Чек-лист подготовки

Отметьте пункты перед началом первого урока.

Готовность: все пункты отмечены — можно начинать урок 1 «Оконные функции».
↑ К оглавлению

7. Источники

  1. Документация Microsoft Learn по языку Transact-SQL: learn.microsoft.com/sql/t-sql.
  2. Itzik Ben-Gan. T-SQL Querying. Оконные функции, обобщённые табличные выражения, логическая обработка запросов.
  3. Grant Fritchey. SQL Server Execution Plans. Чтение планов выполнения запросов.
↑ К оглавлению

8. Что дальше

  • Первый урок: 1. Оконные функции
  • Ответы на контрольные вопросы всех уроков собраны в статье «Ответы на контрольные вопросы курса».

Проверено: 23.10.2026

↑ К оглавлению
Пятница, 23 октября 2026