Операции агрегации в представлении игнорируют индекс [закрыто]

8

Сценарий

Когда-то в небольшой компании, которая участвовала в процессе 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 мс.

Некластерный индекс, Legacy

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

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:

Помогите мне понять, что мне нужно сделать, чтобы запросы агрегации в первом представлении, которое я упомянул, выполнялись за разумное время с разумным использованием ресурсов ввода-вывода.

Avarkx
источник
3
Неиндексированное представление не хранит никаких данных, и вы не можете индексировать представление с помощью подзапросов, объединений и т. Д. Я думаю, что вам необходимо рассмотреть материализацию данных по-другому, например разделить это представление на два представления. и затем опрашивая их или вообще обходя взгляд.
Аарон Бертран
Я думаю, что пытаюсь добиться того, что вы предлагаете, но я не совсем понимаю, что мне нужно делать. Я думаю, что унаследованное представление, которое мне удалось проиндексировать и материализовать в качестве лейкопластыря, кажется довольно грубым решением проблемы ограничения подзапросов, и хотя оно работает более или менее в текущем состоянии, оно очень восприимчиво к дополнительным ползучесть Я борюсь с идеей настройки процесса для заполнения совершенно новой базовой таблицы после импорта и изменения представления для ссылки на него.
Аварк

Ответы:

4

Перезапись NOT EXISTSпо DISTINCTсравнению с неравномерным соединением позволяет индексировать представление, но есть веские причины, которые обычно не делают.

План выполнения, сгенерированный для построения индекса на представлении, неизбежно ужасен. Неравенство вызывает физическое объединение вложенных циклов, которое, за исключением одного значения, является перекрестным соединением. Свертывание продукта с отдельной или эквивалентной группой по произведет правильные результаты, предполагая, что столбец соединения не обнуляется (как в примере кода), но он никогда не будет эффективным. Эта неэффективность будет только усугубляться с течением времени, и соответствующие таблицы станут больше.

Подобные проблемы влияют на план выполнения для любого оператора DML, который влияет на таблицу, на которую ссылается представление (поскольку представление должно быть постоянно синхронизировано с базовыми таблицами в SQL Server). Посмотрите на план выполнения, созданный для добавления или изменения одной строки в любой таблице, чтобы понять, что я имею в виду.

На высоком уровне проблема, с которой вы сталкиваетесь, заключается в том, что оптимизатор запросов SQL Server не всегда создает хорошие планы для представлений, которые включают UNION ALL. Многие из оптимизаций, которые мы принимаем как должное (например, MAX-> TOP (1)), просто не реализованы в рамках union all.

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

Если у вас есть место, одним из решений является ведение completeи legacyсоздание таблиц отдельно (включая логику несуществования). Это приводит к дублированию данных и сопряжено с проблемами синхронизации, но, по моему опыту, их гораздо проще решить надежно, чем пытаться получить объединенные представления для создания хороших планов выполнения для широкого диапазона запросов при всех (или даже в большинстве) обстоятельствах.

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

Пол Уайт 9
источник
Спасибо вам и @AaronBertrand за ваш вклад. Ваши идеи и предложения очень ценятся! Я, вероятно, в конечном итоге вручную перенесу данные старой таблицы в новую таблицу, чтобы я мог изменить представление так, чтобы больше не нужно было объединяться со старой таблицей. Теоретически, тогда я смогу полностью удалить устаревшую таблицу. При таком подходе будут другие проблемы, но, как вы упомянули, возможно, эти проблемы будут более управляемыми в долгосрочной перспективе, поскольку то, что я делаю, очевидно, не будет работать хорошо, никогда.
Avarkx