Оптимизация соединения на большом столе


10

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

select
    b.stuff,
    a.added,
    a.value
from
    dbo.hugetable a
    inner join
    #smalltable b on a.fk = b.pk
where
    a.added between @start and @end;

См. Ниже определения используемых таблиц и индексов.

План выполнения указывает, что вложенный цикл используется на #smalltable, и что сканирование индекса по hugetarian выполняется 480 раз (для каждой строки в #smalltable). Это кажется мне обратным, поэтому я попытался использовать вместо этого объединение слиянием:

select
    b.stuff,
    a.added,
    a.value
from
    dbo.hugetable a with(index = ix_hugetable)
    inner merge join
    #smalltable b with(index(1)) on a.fk = b.pk
where
    a.added between @start and @end;

Указанный индекс (полное определение см. Ниже) охватывает столбцы fk (предикат соединения), добавленные (используемые в предложении where) и id (бесполезные) в порядке возрастания и включающие значение .

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

Любое руководство приветствуется. Дополнительная информация предоставляется при необходимости.

Обновление (2011/06/02)

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

select
    b.stuff,
    datediff(month, 0, a.added),
    count(a.value),
    sum(case when a.value > 0 else 1 end) -- this triples the running time!
from
    dbo.hugetable a
    inner join
    #smalltable b on a.fk = b.pk
group by
    b.stuff,
    datediff(month, 0, a.added);

В настоящее время hugetable имеет кластерный индекс pk_hugetable (added, fk)(первичный ключ), а некластеризованный индекс идет другим путем ix_hugetable (fk, added).

Без четвертого столбца, описанного выше, оптимизатор использует соединение с вложенным циклом, как и раньше, используя #smalltable в качестве внешнего ввода и поиск по некластеризованному индексу в качестве внутреннего цикла (повторное выполнение 480 раз). Меня беспокоит несоответствие между предполагаемыми строками (12 958,4) и фактическими строками (74 668 468). Относительная стоимость этих поисков составляет 45%. Продолжительность, однако, меньше минуты.

С 4-й колонкой время работы увеличивается до 4 минут. На этот раз он ищет в кластеризованном индексе (2 выполнения) ту же относительную стоимость (45%), агрегирует с помощью совпадения хеша (30%), затем выполняет хеш-соединение на #smalltable (0%).

Я не уверен относительно моего следующего курса действий. Меня беспокоит то, что ни поиск по диапазону дат, ни предикат объединения не гарантируются, или даже все, что может существенно сократить набор результатов. В большинстве случаев диапазон дат будет обрезать только 10-15% записей, а внутреннее объединение на fk может отфильтровывать, возможно, 20-30%.


В соответствии с просьбой Уилла А, результаты sp_spaceused:

name      | rows      | reserved    | data        | index_size  | unused
hugetable | 261774373 | 93552920 KB | 18373816 KB | 75167432 KB | 11672 KB

#smalltable определяется как:

create table #endpoints (
    pk uniqueidentifier primary key clustered,
    stuff varchar(6) null
);

В то время как dbo.hugetable определяется как:

create table dbo.hugetable (
    id uniqueidentifier not null,
    fk uniqueidentifier not null,
    added datetime not null,
    value decimal(13, 3) not null,

    constraint pk_hugetable primary key clustered (
        fk asc,
        added asc,
        id asc
    )
    with (
        pad_index = off, statistics_norecompute = off,
        ignore_dup_key = off, allow_row_locks = on,
        allow_page_locks = on
    )
    on [primary]
)
on [primary];

Со следующим определенным индексом:

create nonclustered index ix_hugetable on dbo.hugetable (
    fk asc, added asc, id asc
) include(value) with (
    pad_index = off, statistics_norecompute = off,
    sort_in_tempdb = off, ignore_dup_key = off,
    drop_existing = off, online = off,
    allow_row_locks = on, allow_page_locks = on
)
on [primary];

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


Не могли бы вы включить результат sp_spaceused 'dbo.hugetable', пожалуйста?
— Будет ли

Готово, добавлено чуть выше начала таблицы определений.
— Быстрый Джо Смит

Это точно так. Его нелепый размер - причина, по которой я смотрю на это.
— Быстрый Джо Смит

Ответы:


5

Ваша ix_hugetableвнешность совершенно бесполезна, потому что:

  • он является кластерным индексом (ПК)
  • INCLUDE не имеет значения, потому что кластеризованный индекс включает все неключевые столбцы (неключевые значения на самом нижнем листе = INCLUDEd = что такое кластеризованный индекс)

Кроме того: - добавлен или fk должен быть первым - ID является первым = мало пользы

Попробуйте изменить кластерный ключ на (added, fk, id)и отпустите ix_hugetable. Вы уже попробовали (fk, added, id). Если ничего другого, вы сэкономите много места на диске и обслуживание индекса

Другим вариантом может быть попытка подсказки FORCE ORDER с указанием порядка таблицы без подсказок JOIN / INDEX. Я стараюсь не использовать подсказки JOIN / INDEX лично, потому что вы удаляете опции для оптимизатора. Много лет назад мне сказали (семинар с гуру SQL), что подсказка FORCE ORDER может помочь, когда у вас огромный стол. JOIN small table: YMMV 7 лет спустя ...

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

Редактировать, после 02 июня обновления

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

Попробуйте изменить индекс NC, чтобы ВКЛЮЧИТЬ столбец значений, чтобы не иметь доступа к столбцу значений для кластеризованного индекса.

create nonclustered index ix_hugetable on dbo.hugetable (
    fk asc, added asc
) include(value)

Примечание. Если значение не имеет значения nullable, оно совпадает с COUNT(*)семантическим. Но для СУММ нужна фактическая стоимость, а не существование .

Например, если вы измените COUNT(value)на COUNT(DISTINCT value) без изменения индекса, он должен снова разорвать запрос, поскольку он должен обрабатывать значение как значение, а не как существование.

Запросу нужно 3 столбца: добавлено, fk, значение. Первые 2 фильтруются / объединяются, как и ключевые столбцы. значение только что используется, поэтому может быть включено. Классическое использование указателя покрытия.


Ха, у меня было в голове, что кластерные и некластеризованные индексы были добавлены в другом порядке. Я не могу поверить, что я не заметил этого, почти так же, как я не могу поверить, что это было настроено в первую очередь. Завтра я изменю кластерный индекс, а потом пойду выпить кофе, пока он перестраивается.
— Быстрый Джо Смит

Я изменил индексирование и использовал команду FORCE ORDER, чтобы уменьшить количество запросов на большую таблицу, но безрезультатно. Мой вопрос был обновлен.
— Быстрый Джо Смит

@Quick Джо Смит: обновил мой ответ
— gbn

Да, я попробовал это вскоре после этого. Поскольку перестройка индекса занимает так много времени, я забыл об этом и сначала подумал, что ускорил его, сделав что-то совершенно не связанное.
— Быстрый Джо Смит

2

Определите индекс hugetableтолько для addedстолбца.

БД будет использовать индекс из нескольких частей (из нескольких столбцов) только в крайней правой части списка столбцов, поскольку он имеет значения, считающиеся слева. Ваш запрос не указывает fkв предложении where первого запроса, поэтому он игнорирует индекс.


План выполнения показывает, что индекс (ix_hugetable) находится в поиске. Или вы говорите, что этот индекс не подходит для запроса?
— Быстрый Джо Смит

Индекс не подходит. Кто знает, как это «с помощью индекса». Опыт подсказывает мне, что это твоя проблема. Попробуйте и расскажите нам, как это происходит.
— Богемный

@Быстрый Джо Смит - ты попробовал предложение @ Богемиана? Что, где результаты?
— Ливен Керсмейкерс

2
Я не согласен: предложение ON сначала обрабатывается логически, и на практике это ГДЕ, поэтому OP сначала должен попробовать оба столбца. Нет индексации по fk вообще = сканирование кластерного индекса или поиск ключа для получения значения fk для JOIN. Можете ли вы добавить некоторые ссылки на поведение, которое вы описали тоже, пожалуйста? Специально для SQL Server, если у вас мало предыдущей истории ответов для этой СУБД. На самом деле, -1 в ретроспективе как я введите этот комментарий
— gbn

2

План выполнения указывает, что вложенный цикл используется на #smalltable, и что сканирование индекса по hugetarian выполняется 480 раз (для каждой строки в #smalltable).

Это тот порядок, который я ожидал бы использовать в оптимизаторе запросов, предполагая, что цикл объединяется в правильном выборе. Альтернатива состоит в том, чтобы выполнить цикл 250M раз и выполнить поиск в таблице #temp каждый раз, что может занять несколько часов / дней.

Индекс, который вы заставляете использовать в соединении MERGE, в значительной степени равен 250 миллионам строк * «размер каждой строки» - не маленький, по крайней мере, несколько ГБ. Судя по sp_spaceusedвыходным данным, «пара ГБ» может быть довольно преуменьшением - объединение MERGE требует, чтобы вы просматривали индекс, который будет очень интенсивно вводить-выводить.


Насколько я понимаю, существует 3 типа алгоритмов соединения, и что соединение слиянием имеет лучшую производительность, когда оба входа упорядочены предикатом соединения. Правильно или нет, это результат, который я пытаюсь получить.
— Быстрый Джо Смит

2
Но это еще не все. Если #smalltable имеет большое количество строк, тогда может быть целесообразно объединение слиянием. Если, как следует из названия, у него небольшое количество строк, то соединение цикла может быть правильным выбором. Представьте, что у #smalltable есть одна или две строки, и они сопоставляются с несколькими строками из другой таблицы - здесь было бы трудно оправдать объединение слиянием.
— Будет ли

Я подумал, что это еще не все; Я просто не знал, что это может быть. Оптимизация базы данных не совсем моя сильная сторона, как вы, наверное, уже догадались.
— Быстрый Джо Смит

@Быстрый Джо Смит - спасибо за sp_spaceused. 75 ГБ индекса и 18 ГБ данных - это не единственный индекс в таблице, ix_hugetable?
— Будет ли

1
+1 будет. Планировщик в настоящее время делает правильную вещь. Проблема заключается в случайном поиске диска из-за того, как ваши таблицы кластеризованы.
— Дени де Бернарди

1

Ваш индекс неверен. Смотрите индексы DOS и DonTS .

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

Попробуйте добавить кластерный индекс на hugetable(added, fk). Это должно заставить планировщика искать подходящие строки из огромной таблицы, а петли вложений или слияния соединяют их с небольшой таблицей.


Спасибо за эту ссылку. Я попробую это, когда завтра доберусь до работы.
— Быстрый Джо Смит
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.