Можно ли повысить производительность запросов в узкой таблице с миллионами строк?

14

У меня есть запрос, который в настоящее время занимает в среднем 2500 мсек. Моя таблица очень узкая, но в ней 44 миллиона строк. Какие варианты у меня есть, чтобы улучшить производительность, или это так хорошо, как это получается?

Запрос

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31'; 

Таблица

CREATE TABLE [dbo].[Heartbeats](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [DeviceID] [int] NOT NULL,
    [IsPUp] [bit] NOT NULL,
    [IsWebUp] [bit] NOT NULL,
    [IsPingUp] [bit] NOT NULL,
    [DateEntered] [datetime] NOT NULL,
 CONSTRAINT [PK_Heartbeats] PRIMARY KEY CLUSTERED 
(
    [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 [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)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]

Поможет ли добавление дополнительных индексов? Если так, то как бы они выглядели? Текущая производительность является приемлемой, поскольку запрос выполняется только изредка, но мне интересно как учебное упражнение, могу ли я что-нибудь сделать, чтобы сделать это быстрее?

ОБНОВИТЬ

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

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats] WITH(INDEX(CommonQueryIndex))
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' 

Добавление правильно выбранного предложения DeviceID также попадает в диапазон 50 мс:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' AND DeviceID = 4;

Если я добавлю ORDER BY [DateEntered], [DeviceID]к исходному запросу, я нахожусь в диапазоне 50 мс:

SELECT TOP 1000 * FROM [CIA_WIZ].[dbo].[Heartbeats]
WHERE [DateEntered] BETWEEN '2011-08-30' and '2011-08-31' 
ORDER BY [DateEntered], [DeviceID];

Все они используют индекс, который я ожидал (CommonQueryIndex), так что, я полагаю, мой вопрос сейчас, есть ли способ заставить этот индекс использоваться в таких запросах, как этот? Или это размер моего стола сбросив оптимизатор слишком много , и я просто должен использовать ORDER BYили намек?

Nate
источник
Я думаю, вы могли бы добавить еще один некластеризованный индекс в DateEntered, который увеличил бы производительность в некоторой степени
Правин
@Praveen Будет ли это в основном так же, как мой существующий индекс? Нужно ли делать что-то особенное, так как в одном поле будет два индекса?
Nate
@Nate, так как таблица называется heartbeat и в ней задействовано 44 миллиона записей, я полагаю, у вас есть тяжелые вставки в эту таблицу? С индексированием вы можете только добавить индекс покрытия, чтобы ускорить. Но, как вы упомянули, вы используете этот запрос только изредка, я настоятельно рекомендую вам не делать этого, если вы делаете тяжелые вставки. Это в основном удваивает загрузку вашей вставки. Вы работаете на корпоративной версии?
Эдвард Дортланд,
Я заметил, что у вас есть deviceID в вашем индексе NC. Возможно ли включить это в ваше предложение where? И приведет ли это к тому, что результат окажется ниже порога? <35 тыс. Записей (без предложения 1000 лучших).
Эдвард Дортланд,
1
последний вопрос, вы всегда вставляете в порядке dateEntered? Или они могут быть не в порядке, поскольку устройства могут вставлять асинхронные данные друг от друга. Вы можете попытаться изменить кластеризованный индекс на столбец DateEntered. Ваши страницы отпуска вашего кластерного индекса теперь 445 страниц. Это удвоилось бы, если бы вы переходили от int к datetime. Но в этом случае это может быть не так уж плохо.
Эдвард Дортланд,

Ответы:

13

Почему оптимизатор не использует ваш первый индекс:

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)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]

Это вопрос селективности столбца [DateEntered].

Вы сказали нам, что в вашей таблице 44 миллиона строк. размер строки:

4 байта для идентификатора, 4 байта для идентификатора устройства, 8 байтов для даты и 1 байт для 4-битных столбцов. это 17 байт + 7 байт для (теги, нулевое растровое изображение, смещение переменной col, количество столбцов) составляет 24 байта на строку.

Это было бы грубым переводом на 140 тыс. Страниц. Для хранения этих 44 миллионов строк.

Теперь оптимизатор может делать две вещи:

  1. Может сканировать таблицу (сканирование кластерного индекса)
  2. Или это может использовать ваш индекс. Для каждой строки в вашем индексе необходимо выполнить поиск по закладкам в кластерном индексе.

Теперь в определенный момент становится все дороже выполнять все эти одиночные поиски в кластеризованном индексе для каждой записи индекса, найденной в вашем некластеризованном индексе. Порогом для этого обычно является общее количество поисков, которое должно превышать 25% и 33% от общего числа страниц таблицы.

Так что в этом случае: 140k / 25% = 35000 строк 140k / 33% = 46666 строк.

(@RBarryYoung, 35k - это 0,08% от общего числа строк, а 46666 - это 0,10%, так что я думаю, что именно здесь и произошла путаница)

Таким образом, если ваше предложение where будет содержать где-то между 35000 и 46666 строк. (Это находится под верхним предложением!) Весьма вероятно, что ваше некластеризованное не будет использовано и будет использовано сканирование кластерного индекса.

Единственные два способа изменить это:

  1. Сделайте вашу оговорку where более избирательной. (если возможно)
  2. Удалите * и выберите только несколько столбцов, чтобы вы могли использовать индекс покрытия.

Теперь убедитесь, что вы можете создать индекс покрытия, даже если вы используете select *. Однако это просто создает огромные накладные расходы для ваших вставок / обновлений / удалений. Нам нужно больше знать о вашей рабочей нагрузке (читай и писать), чтобы убедиться, что это лучшее решение.

Переход от datetime к smalldatetime уменьшает размер кластеризованного индекса на 16%, а размер некластеризованного индекса - на 24%.

Эдвард Дортланд
источник
порог сканирования, как правило, намного ниже этого (10% или даже ниже), однако, поскольку диапазон составляет один день по сравнению с годом ранее, он не должен даже превышать этот порог. И сканирование кластерного индекса не дано, так как был добавлен индекс покрытия. Поскольку этот индекс делает предложение WHERE SARG-совместимым, он должен быть предпочтительным.
RBarryYoung
@RBarryYoung Я пытался объяснить, почему некластерный индекс в [EnteredDate], [DeviceID] не использовался в первую очередь. Что касается порога, я думаю, что мы оба согласны, я говорю только с точки зрения страницы. Я изменю свой ответ, чтобы сделать его более понятным.
Эдвард Дортланд
Изменил ответ, чтобы было более понятно, на что я отвечаю. Я не могу объяснить, почему индекс покрытия, предложенный @RBarryYoung, не используется. Я проверил это на миллионе строк только здесь, и оптимизатор, используя индекс покрытия.
Эдвард Дортланд,
Спасибо за очень исчерпывающий ответ, имеет большой смысл. Что касается рабочей нагрузки, таблица имеет 150-300 вставок за 5-минутный период и несколько чтений в день для целей отчетности.
конец
Накладные расходы на индекс покрытия не очень значительны, учитывая, что это узкая таблица, а «покрытие» является лишь дополнением к уже существующему индексу, который уже включал большую часть строки.
RBarryYoung
8

Есть ли конкретная причина, по которой ваш ПК кластеризован? Многие люди делают это, потому что это по умолчанию, или они думают, что PK должны быть кластеризованы. Нет так. Кластерные индексы обычно лучше всего подходят для запросов диапазона (как этот) или для внешнего ключа дочерней таблицы.

Эффект кластеризованного индекса состоит в том, что он объединяет все данные вместе, поскольку данные хранятся в конечных узлах дерева кластера b. Таким образом, предполагая, что вы не запрашиваете «слишком широкий» диапазон, оптимизатор будет точно знать, какая часть дерева b содержит данные, и ему не придется находить идентификатор строки, а затем переходить туда, где эти данные есть (как и при работе с индексом NC). Что такое «слишком широкий» диапазон? Смешной пример - запрос данных за 11 месяцев из таблицы, в которой есть записи за год. Извлечение данных за один день не должно быть проблемой, если предположить, что ваша статистика актуальна. (Тем не менее, у оптимизатора могут возникнуть проблемы, если вы ищете вчерашние данные и не обновляли статистику в течение трех дней.)

Поскольку вы выполняете запрос «SELECT *», движку нужно будет вернуть все столбцы в таблице (даже если кто-то добавит новый столбец, который в данный момент не требуется вашему приложению), так что индекс покрытия или индекс с включенными столбцами мало поможет, если вообще. (Если вы включаете каждый столбец из таблицы в индекс, вы делаете что-то не так.) Оптимизатор, вероятно, будет игнорировать эти индексы NC.

Так что делать?

Мое предложение было бы удалить индекс NC, изменить кластеризованный PK на некластеризованный и создать кластерный индекс на [DateEntered]. Чем проще, тем лучше, пока не доказано обратное.

пролив дарина
источник
Предполагая, что строки вставляются в возрастающем порядке, это самый простой ответ - но вставка в нелинейном порядке приведет к фрагментации.
Кирк Бродхерст
Добавление данных в любую структуру b-дерева приведет к потере баланса. Даже если вы добавляете строки в кластерном порядке, индексы потеряют баланс. Переиндексация таблиц удаляет фрагментацию, и любой администратор БД скажет вам, что таблицы должны быть переиндексированы после того, как «достаточно» данных было добавлено в таблицу. (Определение «достаточно» может быть обсуждено, или «когда» может быть обсуждением.) Я не вижу ничего в вопросе, который говорит, что повторная индексация не может быть выполнена по какой-то причине.
Дарин пролив
4

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

CREATE NONCLUSTERED INDEX [CommonQueryIndex] ON [dbo].[Heartbeats] 
(
    [DateEntered] ASC,
    [DeviceID] ASC
)INCLUDE (ID, IsWebUp, IsPingUp, IsPUp)
 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]

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

RBarryYoung
источник
Я только что попробовал это, и я все еще нахожусь в том же месте, 2500 мс ожидания ответа сервера и 10 мс времени клиента.
Нейт
Опубликовать план запроса.
RBarryYoung
Похоже, что он использует кластерный индекс. (ВЫБРАТЬ Стоимость: 0% <- Top Стоимость: 20% <- Индекс кластерного сканирования PK_Heartbeats Стоимость: 80%)
Nate
Да, это не так, некоторые вещи сбрасывают статистику / оптимизатор. Добавьте подсказку, чтобы заставить его использовать новый индекс.
RBarryYoung
@ Макс Вернон: Возможно, но это должно было быть отмечено в плане запроса.
RBarryYoung
3

Я бы посмотрел на это немного по-другому.

  • Да, я знаю, что это старая тема, но я заинтригован.

Я бы сбросил столбец datetime - изменил бы его на int. Имейте таблицу поиска или сделайте преобразование для своей даты.

Дамп кластеризованного индекса - оставьте его в виде кучи и создайте некластеризованный индекс в новом столбце INT, который представляет дату. то есть сегодня будет 20121015. Этот порядок важен. В зависимости от того, как часто вы загружаете таблицу, посмотрите на создание этого индекса в порядке DESC. Стоимость обслуживания будет выше, и вы захотите ввести коэффициент заполнения или разбиение. Разбиение также поможет сократить время выполнения.

И наконец, если вы можете использовать SQL 2012, попробуйте использовать SEQUENCE - он превзойдет identity () для вставок.

Джереми Лоуэлл
источник
Интересное решение. Хотя это не очевидно из моего вопроса, временная часть DateTime очень важна. Обычно я запрашиваю на основе даты, чтобы просмотреть определенное время в течение этого периода. Как бы вы скорректировали это решение с учетом этого?
конец
В этом случае сохраните столбец datetime, добавьте столбец int для даты (поскольку ваш диапазон основан на элементе date, а не на элементе time). Вы также можете рассмотреть возможность использования типа данных TIME, а затем эффективно разделить время на части от даты. Таким образом, ваш объем данных уменьшается, и у вас все еще есть элемент времени столбца.
Джереми Лоуэлл
1
Я не уверен, почему я пропустил это раньше, но использую сжатие строк в кластеризованном индексе и некластеризованном индексе. Я только что провел быстрый тест с вашей таблицей, и вот что я нашел: я создал набор данных (5,8 миллионов строк) в таблице, определенной выше. Я сжал (row) кластерный и некластеризованный индекс. логическое чтение, основанное на вашем точном запросе, уменьшилось с 2074 до 1433. Это значительное снижение, и я уверен, что только это поможет вам - и это очень низкий риск.
Джереми Лоуэлл