Проблема:
При создании таблиц журналирования или таблиц только для вставки в SQL Server мы часто храним большие строки, такие как URL-адреса, имена хостов или сообщения об ошибках. По мере роста таблицы мы понимаем, что могли бы обработать эти повторяющиеся значения более эффективно. Хранение дублирующихся значений в миллионах строк неэффективно с точки зрения хранения и может привести к медленным запросам поиска.
Решение:
Одно из решений этой проблемы – использование динамических таблиц размерностей. Обычно таблица размерностей предварительно генерируется с известными или медленно изменяющимися областями данных. Однако в случаях, когда новые значения постоянно добавляются в систему, мы можем генерировать таблицу размерностей на лету и заполнять ее только новыми значениями размерности, с которыми мы сталкиваемся.
Рассмотрим пример таблицы журналирования:
CREATE TABLE dbo.ExceptionLog
(
LogID bigint IDENTITY(1,1),
Hostname nvarchar(64),
URI nvarchar(255),
ErrorMessage nvarchar(255),
EventTime datetime2(0) NOT NULL DEFAULT sysutcdatetime(),
CONSTRAINT PK_ExceptionLog PRIMARY KEY (LogID)
);По мере роста таблицы журналирования мы видим, что широкие столбцы (Hostname, URI и ErrorMessage) занимают значительное количество места. Чтобы оптимизировать это, мы можем создать отдельные таблицы размерностей для каждого из этих столбцов:
CREATE TABLE dbo.ExceptionHostnames
(
HostnameID bigint IDENTITY(1,1),
Hostname nvarchar(64),
CONSTRAINT PK_ExceptionHostnames PRIMARY KEY (HostnameID)
);
CREATE TABLE dbo.ExceptionURIs
(
URIID bigint IDENTITY(1,1),
URI nvarchar(255),
CONSTRAINT PK_ExceptionURIs PRIMARY KEY (URIID)
);
CREATE TABLE dbo.ExceptionErrorMessages
(
ErrorMessageID bigint IDENTITY(1,1),
ErrorMessage nvarchar(255),
CONSTRAINT PK_ExceptionErrorMessages PRIMARY KEY (ErrorMessageID)
);Затем мы можем добавить столбцы с возможными внешними ключами в таблицу журналирования, которые ссылаются на таблицы размерностей:
ALTER TABLE dbo.ExceptionLog ADD
HostnameID bigint NULL FOREIGN KEY
REFERENCES dbo.ExceptionHostnames(HostnameID),
URIID bigint NULL FOREIGN KEY
REFERENCES dbo.ExceptionURIs(URIID),
ErrorMessageID bigint NULL FOREIGN KEY
REFERENCES dbo.ExceptionErrorMessages(ErrorMessageID);Чтобы гарантировать правильную обработку новых значений, мы можем создать триггер AFTER INSERT, который генерирует новые идентификаторы для новых значений и обновляет таблицу журналирования этими новыми идентификаторами:
CREATE TRIGGER dbo.ExceptionLog_DivertDimensions
ON dbo.ExceptionLog
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
-- генерация новых идентификаторов
INSERT dbo.ExceptionHostnames(Hostname)
SELECT Hostname FROM inserted AS i WHERE NOT EXISTS
(
SELECT 1 FROM dbo.ExceptionHostnames
WHERE Hostname = i.Hostname
) GROUP BY Hostname;
INSERT dbo.ExceptionURIs(URI)
SELECT URI FROM inserted AS i WHERE NOT EXISTS
(
SELECT 1 FROM dbo.ExceptionURIs
WHERE URI = i.URI
) GROUP BY URI;
INSERT dbo.ExceptionErrorMessages(ErrorMessage)
SELECT ErrorMessage FROM inserted AS i WHERE NOT EXISTS
(
SELECT 1 FROM dbo.ExceptionErrorMessages
WHERE ErrorMessage = i.ErrorMessage
) GROUP BY ErrorMessage;
-- обновление идентификаторов
UPDATE l SET l.HostnameID = h.HostnameID
FROM dbo.ExceptionLog AS l
INNER JOIN dbo.ExceptionHostnames AS h
ON l.Hostname = h.Hostname
INNER JOIN inserted AS i ON i.LogID = l.LogID
WHERE l.HostnameID IS NULL;
UPDATE l SET l.URIID = u.URIID
FROM dbo.ExceptionLog AS l
INNER JOIN dbo.ExceptionURIs AS u
ON l.URI = u.URI
INNER JOIN inserted AS i ON i.LogID = l.LogID
WHERE l.URIID IS NULL;
UPDATE l SET l.ErrorMessageID = m.ErrorMessageID
FROM dbo.ExceptionLog AS l
INNER JOIN dbo.ExceptionErrorMessages AS m
ON l.ErrorMessage = m.ErrorMessage
INNER JOIN inserted AS i ON i.LogID = l.LogID
WHERE l.ErrorMessageID IS NULL;
ENDПосле установки триггера мы можем заполнить таблицы размерностей всеми значениями, которые мы видели до сих пор. Мы можем делать это постепенно, используя дружественные к журналу, непрерывные пакеты:
INSERT dbo.ExceptionHostnames(Hostname)
SELECT Hostname FROM dbo.ExceptionLog AS src WHERE NOT EXISTS
(
SELECT 1 FROM dbo.ExceptionHostnames
WHERE Hostname = src.Hostname
) GROUP BY Hostname;
INSERT dbo.ExceptionURIs(URI)
SELECT URI FROM dbo.ExceptionLog AS src WHERE NOT EXISTS
(
SELECT 1 FROM dbo.ExceptionURIs
WHERE URI = src.URI
) GROUP BY URI;
INSERT dbo.ExceptionErrorMessages(ErrorMessage)
SELECT ErrorMessage FROM dbo.ExceptionLog AS src WHERE NOT EXISTS
(
SELECT 1 FROM dbo.ExceptionErrorMessages
WHERE ErrorMessage = src.ErrorMessage
) GROUP BY ErrorMessage;Наконец, мы можем обновить таблицу журналирования сгенерированными идентификаторами:
UPDATE l SET l.HostnameID = h.HostnameID
FROM dbo.ExceptionLog AS l
INNER JOIN dbo.ExceptionHostnames AS h
ON l.Hostname = h.Hostname
WHERE l.HostnameID IS NULL;
UPDATE l SET l.URIID = u.URIID
FROM dbo.ExceptionLog AS l
INNER JOIN dbo.ExceptionURIs AS u
ON l.URI = u.URI
WHERE l.URIID IS NULL;
UPDATE l SET l.ErrorMessageID = m.ErrorMessageID
FROM dbo.ExceptionLog AS l
INNER JOIN dbo.ExceptionErrorMessages AS m
ON l.ErrorMessage = m.ErrorMessage
WHERE l.ErrorMessageID IS NULL;Если мы можем изменить код приложения, мы можем создать представление, которое обрабатывает соединения с таблицами размерностей и производит те же выходные столбцы, что и раньше. Приложение может затем читать из этого представления вместо прямого чтения из таблицы журналирования:
CREATE VIEW dbo.vExceptionLog
AS
SELECT l.LogID,
h.Hostname,
u.URI,
m.ErrorMessage,
l.EventTime
FROM dbo.ExceptionLog AS l
INNER JOIN dbo.ExceptionHostnames AS h
ON l.HostnameID = h.HostnameID
INNER JOIN dbo.ExceptionURIs AS u
ON l.URIID = u.URIID
INNER JOIN dbo.ExceptionErrorMessages AS m
ON l.ErrorMessageID = m.ErrorMessageID;Если мы не можем немедленно изменить код приложения, мы можем продолжать заполнять широкие столбцы и постепенно обрабатывать изменение. Во второй части мы рассмотрим способы минимизации влияния на запись и защиты пользователей от основного изменения, пока код приложения не будет обновлен.
Реализуя динамические таблицы размерностей и обновляя таблицу журналирования суррогатными ключами, мы можем значительно сократить требования к хранению и улучшить производительность запросов поиска. Эта оптимизация особенно ценна для таблиц журналирования, которые могут расти экспоненциально и непредсказуемо.
Следите за второй частью этой серии, где мы рассмотрим стратегии обработки изменений, когда мы не можем немедленно обновить код приложения.
Статья обновлена: 2022-04-08