Skip to content

Индексированные представления - панацея производительности или беда

Пересказ статьи Jason Brimhall. Indexed Views – Performance Panacea or Plight



Индексированное представление - это представление, когда результирующий набор запроса (определения представления) материализуется вместо результирующего набора виртуальной таблицы стандартного (неиндексированного) представления. Часто приходится видеть, что индексированное представление создается с целью улучшить производительность. И слишком часто индексированное представление создается без учета накладных расходов.

Индексированные представления


В этой статье я надеюсь осветить некоторые наиболее важные накладные расходы, которые часто упускаются из вида, когда индексированное представление рассматривается как панацея производительности.

Установка


Для демонстрации игнорируемых затрат индексированных представлений я решил использовать базу данных AdventureWorks2014 (если у вас нет этой тестовой базы данных, вы можете скачать её копию отсюда). Я создам представление в этой базе данных, а затем добавлю к нему несколько индексов. Прежде чем поделиться всей настройкой, я, как все правильные администраторы баз данных, должен определить необходимый базовый уровень. Для этого базового уровня я не делаю ничего ужасно сложного. Я просто укажу размеры таблиц и информацию для сохранения всей базы данных.

Вот мои результаты относительно размера объектов в базе данных AdventureWorks2014:



Эти результаты показывают, что наибольший объект в базе данных - это таблица Person.Person, занимающая всего около 30Мб. Не такая уж большая, да и вся база не столь велика. Давайте посмотрим, что произойдет, когда я добавлю материализованное представление на базе таблицы Person.Person. Вот определение этого представления вместе с индексами, которые я добавлю для улучшения производительности некоторых важных рабочих запросов.

USE [AdventureWorks2014];
GO
CREATE VIEW [Person].[vPerson]
WITH SCHEMABINDING
AS
SELECT pp.BusinessEntityID
, pp.FirstName
, pp.LastName
, sp.[StateProvinceID]
, sp.[StateProvinceCode]
, sp.[IsOnlyStateProvinceFlag]
, sp.[Name] AS [StateProvinceName]
, sp.[TerritoryID]
, cr.[CountryRegionCode]
, cr.[Name] AS [CountryRegionName]
, pbe.AddressTypeID
, pp.AdditionalContactInfo
, pp.Demographics
FROM [Person].[StateProvince] sp
INNER JOIN [Person].[CountryRegion] cr
ON sp.[CountryRegionCode] = cr.[CountryRegionCode]
INNER JOIN Person.Address pa
ON sp.StateProvinceID = pa.StateProvinceID
INNER JOIN Person.BusinessEntityAddress pbe
ON pbe.AddressID = pa.AddressID
INNER JOIN Person.Person pp
ON pp.BusinessEntityID = pbe.BusinessEntityID
WHERE pbe.AddressTypeID = 2;
GO
CREATE UNIQUE CLUSTERED INDEX CI_vPersonID ON Person.vPerson(BusinessEntityID);
CREATE INDEX IX_vPersonName ON Person.vPerson(FirstName,LastName);
CREATE INDEX IX_vPersonState ON Person.vPerson(FirstName,LastName,StateProvinceID,StateProvinceCode,StateProvinceName);

После выполнения всего этого кода для создания нового представления с индексами, я имею следующие результаты по размерам:



Создание этого представления понизило кучу хранилищ. Оно поднялось сразу на второе место в списке самых больших объектов в этой базе данных. Вы можете увидеть разницу, сравнив выделенные строки с предыдущим рисунком. Представление vPerson выделено красным на втором рисунке.

Конечно, это может быть надуманный пример, и люди не делают такого в реальном мире, правильно? Ответ простой: НЕТ! ЭТО ПРОИСХОДИТ. Я очень часто наблюдаю подобные ситуации. Слишком часто большие текстовые поля добавляются в индексированное представление, чтобы ускорить выборку. Я сымитировал это добавлением двух XML-столбцов из таблицы Person.Person. Это определенно лишнее, поскольку простое соединение с таблицей на основе BusinessEntityID даст мне эти два столбца. Все чего я добился - это продублировал хранящиеся данные и по самым скромным подсчетам увеличил стоимость хранения на 25% для этой небольшой базы. Для информации, в таблице Person.Person и в этом новом представлением имеется по 13 столбцов.

Я называю увеличение на 25% стоимости хранения значительным. Увеличение хранилища на 25% для единственного материализованного представления не является чем-то особенным. Я видел мультитерабайтные базы данных, в которых 25% общего хранилища данных занимало единственное представление. Если вы испытываете недостаток свободного пространства, любое неожиданное увеличение объема данных может вызвать нелинейный рост требований к данным только лишь из-за наличия индексированных представлений. Принимайте это в расчет, выбирая между настройкой производительности кода и созданием индексированного представления из существующего.

Производительность лейкопластыря


Я связывал использование индексированных представлений со средством для повышения производительности. Это делается преобразованием тормозящего запроса в представление. Идея состоит в том, что запрос будет выполняться быстрее, поскольку создается конкретный индекс под соответствующий набор данных. Как правило, выигрыш в производительности от использования индексированного представления для маскировки плохого запроса ощущается, когда запрос сложный. К сожалению, запрос, рассматриваемый здесь не очень сложный. Поскольку это так, давайте сначала посмотрим на план выполнения другого запроса, который выполняется ужасно медленно, хотя тоже не очень сложен:



Имеем несколько табличных спулов и сканирования индекса. Видно, что максимальную стоимость в пакете имеет merge join с операцией соединения "многие ко многим". Индексированное представление в этом случае дает мне весьма существенное улучшение на этом запросе (взгляните на эту предполагаемую стоимость, а затем на оценку числа строк). Что если бы я мог бы настроить немного запрос и избежать затрат на хранение? Давайте посмотрим!

Посмотрите на базовый запрос для этого нового представления:

SELECT pp.BusinessEntityID
, pp.FirstName
, pp.LastName
, sp.[StateProvinceID]
, sp.[StateProvinceCode]
, sp.[IsOnlyStateProvinceFlag]
, sp.[Name] AS [StateProvinceName]
, sp.[TerritoryID]
, cr.[CountryRegionCode]
, cr.[Name] AS [CountryRegionName]
, pbe.AddressTypeID
, pp.AdditionalContactInfo
, pp.Demographics
FROM [Person].[StateProvince] sp
INNER JOIN [Person].[CountryRegion] cr
ON RTRIM(LTRIM(sp.[CountryRegionCode])) = RTRIM(LTRIM(cr.[CountryRegionCode]))
INNER JOIN Person.Address pa
ON RTRIM(LTRIM(sp.StateProvinceID)) = RTRIM(LTRIM(pa.StateProvinceID))
INNER JOIN Person.BusinessEntityAddress pbe
ON RTRIM(LTRIM(pbe.AddressID)) = RTRIM(LTRIM(pa.AddressID)) INNER JOIN Person.Person pp
ON RTRIM(LTRIM(pp.BusinessEntityID)) = RTRIM(LTRIM(pbe.BusinessEntityID))
WHERE RTRIM(LTRIM(pbe.AddressTypeID)) = 2;

Глядя на запрос, вы можете спросить себя, почему так много функций trim на каждом соединении? Этот экстрим можно легко пофиксить. Ни одно из полей, участвующих в соединениях, не требуется тримить, поскольку они все числового типа. Начав с этого и удалив все использования этой функции в соединениях, мы должны получить значительное улучшение скорости запроса и без использования представления. Тестирование даст результаты производительности, аналогичные первому представлению в этой статье. Это небольшое изменение приводит к 1-секундному выполнению запроса вместо почти 90 секунд. Это огромный прирост, который получен без дополнительных расходов на увеличение размера хранилища.

Встречаю ли я такие вещи на практике? Да! Потратьте немного времени на устранение корневой проблемы вместо поиска быстрого решения, и производительность может вас поразить. Хотя бывают случаи, когда индексированное представление может быть абсолютным требованием. Бывают случаи, когда это оправдано, и нет никакого другого способа обойти его. Если индексированное представление является абсолютной необходимостью, то необходимо учитывать еще несколько факторов.

Исчезающий закон


Выше я показал, что индексированное представление требует места в хранилище, и что созданное новое представление сразу потребовало себе второе по размеру место в базе данных. Теперь, чтобы перейти к следующему моменту, я еще раз покажу, что индексы присутствуют и занимают место на диске. Это можно сделать с помощью следующего запроса:

SELECT o.name AS ObjName
, i.name AS IdxName
, SUM(ps.page_count) AS page_count
, SUM(ps.record_count) AS record_count
, SUM(ps.page_count) / 128.0 AS SizeMB
FROM sys.indexes i
INNER JOIN sys.objects o
ON i.object_id = o.object_id
INNER JOIN sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'detailed') ps
ON ps.object_id = o.object_id
WHERE o.name LIKE 'vperson%'
GROUP BY o.name
, i.name;

Результаты этого запроса выглядят подобно следующим (напомню, что я создал новое индексированное представление для второго демонстрационного примера, и результаты относятся к этому второму представлению):



Теперь, когда мы знаем, что индекс присутствует и занимает место, пришло время внести изменения в представление и посмотреть, что произойдет. Я исхожу из ранее сделанного утверждения, что включение двух столбцов XML в представление совершенно не нужно и является причиной слишком большого потребления пространства. Я могу воспользоваться преимуществами представления за небольшую цену, если я ИЗМЕНЮ представление, удалив эти столбцы. Итак, я выполняю следующий оператор ALTER:

ALTER VIEW [Person].[vPerson2]
WITH SCHEMABINDING
AS
SELECT pp.BusinessEntityID
, pp.FirstName
, pp.LastName
, sp.[StateProvinceID]
, sp.[StateProvinceCode]
, sp.[IsOnlyStateProvinceFlag]
, sp.[Name] AS [StateProvinceName]
, sp.[TerritoryID]
, cr.[CountryRegionCode]
, cr.[Name] AS [CountryRegionName]
, pbe.AddressTypeID
FROM [Person].[StateProvince] sp
INNER JOIN [Person].[CountryRegion] cr
ON RTRIM(LTRIM(sp.[CountryRegionCode])) = RTRIM(LTRIM(cr.[CountryRegionCode]))
INNER JOIN Person.Address pa
ON RTRIM(LTRIM(sp.StateProvinceID)) = RTRIM(LTRIM(pa.StateProvinceID))
INNER JOIN Person.BusinessEntityAddress pbe
ON RTRIM(LTRIM(pbe.AddressID)) = RTRIM(LTRIM(pa.AddressID))
INNER JOIN Person.Person pp
ON RTRIM(LTRIM(pp.BusinessEntityID)) = RTRIM(LTRIM(pbe.BusinessEntityID))
WHERE RTRIM(LTRIM(pbe.AddressTypeID)) = 2;

Обратите внимание на то, сколько времени выполняется команда ALTER - почти мгновенно. Давайте проверим индексы, чтобы убедиться, что они все еще в рабочем порядке и не требуют дефрагментации. Для этого я снова запущу наш запрос, чтобы проверить индексы и их размеры.

SELECT o.name AS ObjName
, i.name AS IdxName
, SUM(ps.page_count) AS page_count
, SUM(ps.record_count) AS record_count
, SUM(ps.page_count) / 128.0 AS SizeMB
FROM sys.indexes i
INNER JOIN sys.objects o
ON i.object_id = o.object_id
INNER JOIN sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'detailed') ps
ON ps.object_id = o.object_id
WHERE o.name LIKE 'vperson%'
GROUP BY o.name
, i.name;

На этот раз меня ждет небольшой сюрприз: у меня больше нет индексов в этом представлении. Изменение определения представления сразу удаляет все индексы, которые были построены на нём. Если это делается в производственной системе, представьте себе последствия и проблемы, которые могут возникнуть.

Заключение


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

Есть еще несколько критических соображений относительно индексированных представлений, которые также часто упускаются из виду: это блокировка, взаимоблокировка (тупик) и обслуживание.

Обратные ссылки

Нет обратных ссылок

Комментарии

Показывать комментарии Как список | Древовидной структурой

Нет комментариев.

Автор не разрешил комментировать эту запись

Добавить комментарий

Enclosing asterisks marks text as bold (*word*), underscore are made via _word_.
Standard emoticons like :-) and ;-) are converted to images.

To prevent automated Bots from commentspamming, please enter the string you see in the image below in the appropriate input box. Your comment will only be submitted if the strings match. Please ensure that your browser supports and accepts cookies, or your comment cannot be verified correctly.
CAPTCHA

Form options

Добавленные комментарии должны будут пройти модерацию прежде, чем будут показаны.