Сценарий
Когда-то в небольшой компании, которая участвовала в процессе ETL, существовала промежуточная база данных, которая действовала как приемный каталог для различных форматов файлов из ряда сторонних источников. E обрабатывался с помощью пакетов DTS, с небольшим количеством контрольных структур для аудита или контроля, но считался «достаточно хорошим» и для всех намерений и целей.
Данные, предоставленные частью E, предназначались для потребления отдельным приложением, разработанным и управляемым несколькими молодыми и способными программистами. Хотя им не хватало опыта или знаний техник хранения данных того времени, они разработали и создали свои собственные процессы T и L из кода приложения. Прорыв, эти молодые разработчики программного обеспечения изобрели то, что посторонние могли бы назвать «менее чем идеальным колесом», но с «Good Enough» в качестве постоянного уровня обслуживания, они смогли обеспечить операционную основу.
Какое-то время все было хорошо в тесно связанной области, когда в промежуточный каталог добавлялись данные десятка третьих сторон, которые, в свою очередь, подпитывались приложением. По мере роста приложения росли и его аппетиты, но с умелыми разработчиками White Knight, которые следили за системой, эти аппетиты были решены быстро, а во многих случаях даже хорошо.
Но золотой век, конечно, не мог длиться вечно. С процветанием, обеспеченным успешной заявкой, бизнес рос и рос. По мере роста среда Stage и приложение были вынуждены расти вместе с ней. Несмотря на всю их бдительность, горстка разработчиков-героев не могла идти в ногу с поддержанием расширяющейся в настоящее время системы, и потребители получили право на свои данные. Уже не это было вопросом того, что им нужно или даже нужно, но население чувствовало, что оно просто заслужило это, требуя еще больше.
Бизнес, вооруженный всего лишь сундуками, полными добычи, вышел на рынок, наняв разработчиков и администраторов для поддержки постоянно растущей системы. Наемники каждого этоса стекались в компанию, но с этим рывком роста мало что могло помешать доступному экспертному руководству. Новые разработчики и администраторы изо всех сил пытались понять тонкости домашнего приготовления, пока разочарования не привели к войне. Каждый отдел начал пытаться решить каждую проблему в одиночку, делая больше, чтобы работать друг против друга, чем работать друг с другом. Один проект или инициатива будут реализованы несколькими различными способами, каждый из которых будет немного отличаться от следующего. Напряжение всего этого оказалось слишком большим для некоторых белых рыцарей, и когда они упали, империя рухнула. Вскоре система была в руинах,
Несмотря на трансформацию этих полей в обещание спагетти-кода, компания выдержала. Это было, в конце концов, «достаточно хорошо».
Соревнование
Еще несколько смен режима и прием на работу позже, я оказываюсь в компании по найму. После великих войн прошло много лет, но нанесенный ущерб все еще очень заметен. Мне удалось устранить некоторые слабые места в E-части системы и добавить некоторые контрольные таблицы под видом обновления пакетов DTS до SSIS, которые сейчас используются некоторыми настоящими профессионалами в области хранилищ данных, когда они создают нормальные и задокументированная замена T и L.
Первым препятствием было импортировать данные из сторонних файлов таким образом, чтобы они не обрезали значения и не изменяли собственные типы данных, а также включали некоторые управляющие ключи для перезагрузки и очистки. Все это было хорошо, но приложения должны были иметь доступ к этим новым таблицам беспроблемным и прозрачным способом. Пакет DTS может заполнять таблицу, которая затем непосредственно читается приложением. Обновления служб SSIS должны выполняться параллельно по причинам обеспечения качества, но эти новые пакеты включают в себя различные управляющие ключи, а также используют схему разбиения, не говоря уже о том, что сами изменения метаданных могут быть достаточно значительными, чтобы в любом случае оправдать создание новой таблицы, поэтому новая таблица использовалась для новых пакетов служб SSIS.
С надежным импортом данных, который сейчас работает и используется командой хранилищ, реальная проблема заключается в предоставлении новых данных приложениям, которые напрямую обращаются к промежуточной среде, с минимальным воздействием (например, «нет») на код приложения. Для этого, я избран вида использования, переименование таблицы , такие , как dbo.DailyTransaction
для dbo.DailyTranscation_LEGACY
и повторное использование dbo.DailyTransaction
имени объекта для представления, которое эффективно только выбираешь все из нынеLEGACY
назначенный стол. Поскольку перезагрузка многолетних данных, содержащихся в этих таблицах, не представляется возможным с точки зрения бизнеса, поскольку новые заполненные и разделенные на SSIS таблицы попадают в производство, старый импорт DTS отключен, и приложения должны иметь возможность получить доступ к новым данным в новых таблицах. На этом этапе представления обновляются, чтобы выбрать данные из новых таблиц (скажем, dbo.DailyTransactionComplete
например), когда они доступны, и выбрать из устаревших таблиц, когда их нет.
По сути, делается что-то вроде следующего:
CREATE VIEW dbo.DailyTransaction
AS SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.DailyTransactionComplete
UNION ALL
SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.DailyTransaction_LEGACY l
WHERE NOT EXISTS ( SELECT 1
FROM dbo.DailyTransactionComplete t
WHERE t.FileDate = l.FileDate );
Хотя это логично, в некоторых случаях агрегации это неэффективно, что обычно приводит к плану выполнения, который выполняет полное сканирование индекса по данным в устаревшей таблице. Это, вероятно, хорошо для нескольких десятков миллионов записей, но не так много для нескольких десятков миллионов записей. Поскольку последнее действительно имеет место, мне пришлось прибегнуть к ... «креативности», что привело меня к созданию индексированного представления.
Вот небольшой тестовый пример, который я настроил, в том числе FileDate
управляющий ключ, перенесенный на DateCode_FK
порт, совместимый с хранилищем данных, для иллюстрации того, как мало я забочусь о том, чтобы запросы к новой таблице в настоящее время обрабатывались :
USE tempdb;
GO
SET NOCOUNT ON;
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DailyTransaction_LEGACY'
AND type = 'U' )
BEGIN
--DROP TABLE dbo.DailyTransaction_LEGACY;
CREATE TABLE dbo.DailyTransaction_LEGACY
(
DailyTransaction_PK BIGINT IDENTITY( 1, 1 ) NOT NULL,
FileDate DATETIME NOT NULL,
Foo INT NOT NULL
);
INSERT INTO dbo.DailyTransaction_LEGACY ( FileDate, Foo )
SELECT DATEADD( DAY, ( 1 - ROW_NUMBER()
OVER( ORDER BY so1.object_id ) - 800 ) % 1000,
CONVERT( DATE, GETDATE() ) ),
so1.object_id % 1000 + so2.object_id % 1000
FROM sys.all_objects so1
CROSS JOIN sys.all_objects so2;
ALTER TABLE dbo.DailyTransaction_LEGACY
ADD CONSTRAINT PK__DailyTrainsaction
PRIMARY KEY CLUSTERED ( DailyTransaction_PK )
WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 100 );
END;
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DailyTransactionComplete'
AND type = 'U' )
BEGIN
--DROP TABLE dbo.DailyTransactionComplete;
CREATE TABLE dbo.DailyTransactionComplete
(
DailyTransaction_PK BIGINT IDENTITY( 1, 1 ) NOT NULL,
DateCode_FK INTEGER NOT NULL,
Foo INTEGER NOT NULL
);
INSERT INTO dbo.DailyTransactionComplete ( DateCode_FK, Foo )
SELECT TOP 100000
CONVERT( INTEGER, CONVERT( VARCHAR( 8 ), DATEADD( DAY,
( 1 - ROW_NUMBER() OVER( ORDER BY so1.object_id ) ) % 100,
GETDATE() ), 112 ) ),
so1.object_id % 1000
FROM sys.all_objects so1
CROSS JOIN sys.all_objects so2;
ALTER TABLE dbo.DailyTransactionComplete
ADD CONSTRAINT PK__DailyTransaction
PRIMARY KEY CLUSTERED ( DateCode_FK, DailyTransaction_PK )
WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 100 );
END;
GO
В моей локальной изолированной программной среде вышеупомянутое дает мне устаревшую таблицу с примерно 4,4 миллионами строк и новую таблицу, содержащую 0,1 миллиона строк, с некоторым перекрытием значений DateCode_FK
/ FileDate
.
А по MAX( FileDate )
сравнению с устаревшей таблицей без дополнительных индексов работает примерно так, как я и ожидал.
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput DATETIME;
SELECT @ConsumeOutput = MAX( FileDate )
FROM dbo.DailyTransaction_LEGACY;
SET STATISTICS IO, TIME OFF;
GO
Таблица «DailyTransaction_LEGACY». Сканирование 1, логическое чтение 9228, физическое чтение 0, чтение с опережением 0, логическое чтение 1, физическое чтение 1, чтение с опережением 0.
Время выполнения SQL Server: время ЦП = 889 мс, прошедшее время = 886 мс.
Бросок простого индекса в таблицу делает вещи намного лучше. Все еще сканирование, но сканирование одной записи вместо 4,4 миллиона записей. Я крут с этим.
CREATE NONCLUSTERED INDEX IX__DailyTransaction__FileDate
ON dbo.DailyTransaction_LEGACY ( FileDate );
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput DATETIME;
SELECT @ConsumeOutput = MAX( FileDate )
FROM dbo.DailyTransaction_LEGACY;
SET STATISTICS IO, TIME OFF;
GO
Время анализа и компиляции SQL Server: время ЦП = 0 мс, прошедшее время = 1 мс. Таблица «DailyTransaction_LEGACY». Сканирование 1, логическое чтение 3, физическое чтение 0, чтение с опережением 0, логическое чтение с бита 0, физическое чтение с бита 0, чтение с опережением чтения 0.
Время выполнения SQL Server: время ЦП = 0 мс, прошедшее время = 0 мс.
И теперь, создавая представление, чтобы разработчикам не приходилось изменять какой-либо код, потому что это, очевидно, конец света, каким мы его знаем. Катаклизм сортов.
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DailyTransaction'
AND type = 'V' )
BEGIN
EXEC( 'CREATE VIEW dbo.DailyTransaction AS SELECT x = 1;' );
END;
GO
ALTER VIEW dbo.DailyTransaction
AS SELECT DailyTransaction_PK, FileDate = CONVERT(
DATETIME, CONVERT( VARCHAR( 8 ), DateCode_FK ), 112 ), Foo
FROM dbo.DailyTransactionComplete
UNION ALL
SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.DailyTransaction_LEGACY l
WHERE NOT EXISTS ( SELECT 1
FROM dbo.DailyTransactionComplete t
WHERE CONVERT( DATETIME, CONVERT( VARCHAR( 8 ),
t.DateCode_FK ), 112 ) = l.FileDate );
GO
Да, подзапрос ужасен, но это не проблема, и я, вероятно, просто создаю постоянный вычисляемый столбец и добавляю для него индекс для этой цели, когда реальная проблема решена. Так что без лишних слов,
Проблема
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput1 DATETIME;
SELECT @ConsumeOutput1 = MAX( FileDate )
FROM dbo.DailyTransaction;
SET STATISTICS IO, TIME OFF;
GO
Время анализа и компиляции SQL Server: время ЦП = 0 мс, прошедшее время = 4 мс. Таблица «DailyTransaction_LEGACY». Сканирование 1, логические операции чтения 11972, физические операции чтения 0, операции чтения с опережением 0, логические операции чтения 0, физические операции чтения 0, математические операции чтения 0. Таблица «Рабочий стол». Сканирование счетчик 0, логическое чтение 0, физическое чтение 0, чтение с опережением 0, логическое чтение с бита 0, физическое чтение с бита 0, чтение с опережением чтения 0. Таблица «Рабочий файл». Сканирование 0, логическое чтение 0, физическое чтение 0, чтение с опережением 0, чтение логического объекта 0, чтение с физического объекта 0, чтение с предварительного чтения 0. Таблица 'DailyTransactionComplete'. Сканирование 2, логическое чтение 620, физическое чтение 0, чтение с опережением 0, логическое чтение с 0, физическое чтение с 0, чтение с опережением 0.
Время выполнения SQL Server: время ЦП = 983 мс, прошедшее время = 983 мс.
О, я вижу, Sql Server пытается сказать мне, что то, что я делаю, идиотично. Хотя я в основном согласен, это не меняет мое положение. Это на самом деле работает блестяще для запросов , где FileDate
на dbo.DailyTransaction
взгляд включается в предикат, но в то время как MAX
план плох достаточно, то TOP
план посылает все это работает на юг. Настоящий юг.
SET STATISTICS IO, TIME ON;
SELECT TOP 10 FileDate
FROM dbo.DailyTransaction
GROUP BY FileDate
ORDER BY FileDate DESC
SET STATISTICS IO, TIME OFF;
GO
Таблица «DailyTransactionComplete». Сканирование 2, логическое чтение 1800110, физическое чтение 0, чтение с опережением 0, чтение логического объекта 0, чтение с физического объекта 0, чтение с предварительного чтения 0. Таблица 'DailyTransaction_LEGACY'. Сканирование 1, логическое чтение 1254, физическое чтение 0, чтение с опережением 0, чтение логического объекта 0, чтение с физического объекта 0, чтение с опережением 0. Таблица «Рабочий стол». Сканирование счетчик 0, логическое чтение 0, физическое чтение 0, чтение с опережением 0, логическое чтение с бита 0, физическое чтение с бита 0, чтение с опережением чтения 0. Таблица «Рабочий файл». Сканирование счетчик 0, логическое чтение 0, физическое чтение 0, чтение с опережением 0, логическое чтение с бита 0, физическое чтение с бита 0, чтение с опережением чтения 0.
Время выполнения SQL Server: время ЦП = 109559 мс, прошедшее время = 109664 мс.
Я упоминал, что стал «креативным» ранее, что, вероятно, вводило в заблуждение. То , что я хотел сказать «глупее» , поэтому мои попытки сделать этот вид работу во время агрегирования операций были создавать представления о dbo.DailyTransactionComplete
и dbo.DailyTransaction_LEGACY
таблицах, схемах привязки и индексе последнего одного, а затем использовать этот вид в другой точке зрения с NOEXPAND
намеком на прежнем виде. Хотя он более или менее работает для того, что ему нужно сделать сейчас, я нахожу, что все «решение» довольно огорчает, и завершается следующим:
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'v_DailyTransactionComplete'
AND type = 'V' )
BEGIN
EXEC( 'CREATE VIEW dbo.v_DailyTransactionComplete AS SELECT x = 1;' );
END;
GO
ALTER VIEW dbo.v_DailyTransactionComplete
AS SELECT DailyTransaction_PK, FileDate = CONVERT( DATETIME,
CONVERT( VARCHAR( 8 ), DateCode_FK ), 112 ),
Foo
FROM dbo.DailyTransactionComplete;
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'v_DailyTransaction_LEGACY'
AND type = 'V' )
BEGIN
EXEC( 'CREATE VIEW dbo.v_DailyTransaction_LEGACY AS SELECT x = 1;' );
END;
GO
ALTER VIEW dbo.v_DailyTransaction_LEGACY
WITH SCHEMABINDING
AS SELECT l.DailyTransaction_PK,
l.FileDate,
l.Foo,
CountBig = COUNT_BIG( * )
FROM dbo.DailyTransaction_LEGACY l
INNER JOIN dbo.DailyTransactionComplete n
ON l.FileDate <> CONVERT( DATETIME, CONVERT( VARCHAR( 8 ),
n.DateCode_FK ), 112 )
GROUP BY l.DailyTransaction_PK,
l.FileDate,
l.Foo;
GO
CREATE UNIQUE CLUSTERED INDEX CI__v_DailyTransaction_LEGACY
ON dbo.v_DailyTransaction_LEGACY ( FileDate, DailyTransaction_PK )
WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 80 );
GO
IF NOT EXISTS ( SELECT 1
FROM sys.objects
WHERE name = 'DailyTransaction'
AND type = 'V' )
BEGIN
EXEC( 'CREATE VIEW dbo.DailyTransaction AS SELECT x = 1;' );
END;
GO
ALTER VIEW dbo.DailyTransaction
AS SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.v_DailyTransactionComplete
UNION ALL
SELECT DailyTransaction_PK, FileDate, Foo
FROM dbo.v_DailyTransaction_LEGACY WITH ( NOEXPAND );
GO
Заставляя оптимизатор использовать индекс обеспечивается индексированным представлением делает MAX
и TOP
проблемы уходят, но там должны быть лучшим способом добиться того, что я пытаюсь сделать здесь. Абсолютно любые предложения / выговоры будут очень цениться!
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput1 DATETIME;
SELECT @ConsumeOutput1 = MAX( FileDate )
FROM dbo.DailyTransaction;
SET STATISTICS IO, TIME OFF;
GO
Таблица v_DailyTransaction_LEGACY. Сканирование 1, логическое чтение 3, физическое чтение 0, чтение с опережением 0, логическое чтение с бита 0, физическое чтение с бита 0, чтение с опережением чтения 0. Таблица 'DailyTransactionComplete'. Сканирование 1, логическое чтение 310, физическое чтение 0, чтение с опережением 0, чтение логического объекта 0, физическое чтение 1, чтение с опережением 0.
Время выполнения SQL Server: время ЦП = 31 мс, прошедшее время = 36 мс.
SET STATISTICS IO, TIME ON;
DECLARE @ConsumeOutput1 DATETIME;
SELECT TOP 10 @ConsumeOutput1 = FileDate
FROM dbo.DailyTransaction
GROUP BY FileDate
ORDER BY FileDate DESC
SET STATISTICS IO, TIME OFF;
GO
Таблица v_DailyTransaction_LEGACY. Сканирование 1, логическое чтение 101, физическое чтение 0, чтение с опережением 0, логическое чтение с 0, физическое чтение с 0, чтение с опережением 0. Таблица «Рабочий стол». Сканирование счетчик 0, логическое чтение 0, физическое чтение 0, чтение с опережением 0, логическое чтение с бита 0, физическое чтение с бита 0, чтение с опережением чтения 0. Таблица «Рабочий файл». Сканирование 0, логическое чтение 0, физическое чтение 0, чтение с опережением 0, чтение логического объекта 0, чтение с физического объекта 0, чтение с предварительного чтения 0. Таблица 'DailyTransactionComplete'. Сканирование 1, логическое чтение 310, физическое чтение 0, чтение с опережением 0, чтение логического объекта 0, физическое чтение 1, чтение с опережением 0.
Время выполнения SQL Server: время ЦП = 63 мс, прошедшее время = 66 мс.
TL; DR:
Помогите мне понять, что мне нужно сделать, чтобы запросы агрегации в первом представлении, которое я упомянул, выполнялись за разумное время с разумным использованием ресурсов ввода-вывода.
источник
Ответы:
Перезапись
NOT EXISTS
поDISTINCT
сравнению с неравномерным соединением позволяет индексировать представление, но есть веские причины, которые обычно не делают.План выполнения, сгенерированный для построения индекса на представлении, неизбежно ужасен. Неравенство вызывает физическое объединение вложенных циклов, которое, за исключением одного значения, является перекрестным соединением. Свертывание продукта с отдельной или эквивалентной группой по произведет правильные результаты, предполагая, что столбец соединения не обнуляется (как в примере кода), но он никогда не будет эффективным. Эта неэффективность будет только усугубляться с течением времени, и соответствующие таблицы станут больше.
Подобные проблемы влияют на план выполнения для любого оператора DML, который влияет на таблицу, на которую ссылается представление (поскольку представление должно быть постоянно синхронизировано с базовыми таблицами в SQL Server). Посмотрите на план выполнения, созданный для добавления или изменения одной строки в любой таблице, чтобы понять, что я имею в виду.
На высоком уровне проблема, с которой вы сталкиваетесь, заключается в том, что оптимизатор запросов SQL Server не всегда создает хорошие планы для представлений, которые включают
UNION ALL
. Многие из оптимизаций, которые мы принимаем как должное (например,MAX
->TOP (1)
), просто не реализованы в рамках union all.Для каждой проблемы, которую вы решаете, вы найдете другой случай, когда нормальная и ожидаемая оптимизация не происходит, что приводит к плану выполнения с отчаянной производительностью. Очевидное решение состоит в том, чтобы избежать использования объединения во взглядах. Как вы реализуете это в вашем случае, зависит от деталей, которые, несмотря на детали в вопросе, вероятно, известны только вам.
Если у вас есть место, одним из решений является ведение
complete
иlegacy
создание таблиц отдельно (включая логику несуществования). Это приводит к дублированию данных и сопряжено с проблемами синхронизации, но, по моему опыту, их гораздо проще решить надежно, чем пытаться получить объединенные представления для создания хороших планов выполнения для широкого диапазона запросов при всех (или даже в большинстве) обстоятельствах.Я уверен, что в SQL Server имеется ряд функций, помогающих синхронизировать данные, в том числе отслеживание изменений, сбор данных изменений, триггеры ... и так далее. Специфика реализации выходит за рамки этого форума. Важным моментом является предоставление оптимизатору базовых таблиц, а не объединение всех представлений.
источник