2016-10-03 15 views
1

У меня есть таблица с этим простым определением:SQL запросы на одной табличном параметре медленно на большом входе

CREATE TABLE Related 
(
    RelatedUser NVARCHAR(100) NOT NULL FOREIGN KEY REFERENCES User(Id), 
    RelatedStory BIGINT NOT NULL FOREIGN KEY REFERENCES Story(Id), 
    CreationTime DateTime NOT NULL, 

    PRIMARY KEY(RelatedUser, RelatedStory) 
); 

с этими индексами:

CREATE INDEX i_relateduserid 
    ON Related (RelatedUserId) INCLUDE (RelatedStory, CreationTime) 

CREATE INDEX i_relatedstory 
    ON Related(RelatedStory) INCLUDE (RelatedUser, CreationTime) 

И мне нужно запросить таблицу для все истории, связанные со списком UserIds, заказанные по времени создания, а затем извлекают только X и пропускают Y.

У меня есть эта хранимая процедура:

CREATE PROCEDURE GetStories 
    @offset INT, 
    @limit INT, 
    @input UserIdInput READONLY 
AS 
BEGIN 
    SELECT RelatedStory 
    FROM Related 
    WHERE EXISTS (SELECT 1 FROM @input WHERE UID = RelatedUser) 
    GROUP BY RelatedStory, CreationTime 
    ORDER BY CreationTime DESC 
    OFFSET @offset ROWS FETCH NEXT @limit ROWS ONLY; 
END; 

С помощью этого пользовательского типа таблицы:

CREATE TYPE UserIdInput AS TABLE 
(
    UID nvarchar(100) PRIMARY KEY CLUSTERED 
) 

Таблица содержит 13 миллионов строк, и получает меня хорошие результаты при использовании нескольких UserIds в качестве входных данных, но очень плохо (30+ секунд) приводит при предоставлении сотни или несколько тысяч пользователей в качестве входных данных. Основная проблема заключается в том, что она использует 63% усилий по сортировке.

Какой индекс мне не хватает? это, кажется, довольно простой запрос на одну таблицу.

+1

Рассматривали ли вы изменение своего ГДЕ СУЩЕСТВУЕТ СОЕДИНЕНИЕ. Объединяются, как правило, лучше, особенно с большими наборами. –

+0

Насколько я могу использовать Google, EXISTS предпочтительнее, если вы проверяете существование, например, здесь предлагается ответ: http://stackoverflow.com/questions/7082449/exists-vs-join-and- use-of-exists- Не так ли? – bech

+0

Да, но, как вы упомянули, производительность страдает по мере расширения вашего набора. Если ключ JOIN не индексируется, вы можете получить лучшие результаты с помощью EXISTS. Мое мышление это ... «Как куриный суп для холода ... не помешало бы попробовать». –

ответ

2

Так что я, наконец, нашел решение.

В то время как у @srutzky были хорошие предложения по нормализации таблиц, сменив NVARCHAR UserId на Integer, чтобы минимизировать стоимость сравнения, это не помогло решить мою проблему. Я определенно сделаю это в какой-то момент для дополнительной теоретической производительности, но я видел очень мало изменений в производительности после ее внедрения сразу с места в карьер.

@Paparazzi предложил добавить индекс для (RelatedStory, CreationTime), и это не помогло мне. Причина была,, что мне также нужно было указать IndexUser, так как это вопрос, и он группирует и заказывает как CreationTime, так и RelatedStory, поэтому все три необходимы. Итак:

CREATE INDEX i_idandtime ON Related (RelatedUser, CreationTime DESC, RelatedStory) 

решить мою проблему, в результате чего мои неприемлемые времена запроса 15+ секунд до основном 1 секунду или пару секунд querytimes.

Я думаю, что дал мне откровение @srutzky отметить:

Помните, что «Включить» столбцы не используются для сортировки или сравнения, только для покрытия.

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

Так что, хотя я не могу отметить ни одно из указанных выше постов как ответ, я хотел бы искренне поблагодарить их за их время.

2

Какие у вас есть ценности для RelatedUser/UID? Почему именно вы используете для этого NVARCHAR(100)? NVARCHAR обычно является ужасным выбором для поля PK/FK. Даже если значение представляет собой простой буквенно-цифровой код (например, ABTY1245), есть более эффективные способы его обработки. Одна из основных проблем с NVARCHAR (и даже с VARCHAR для этой конкретной проблемы) заключается в том, что, если вы не используете двоичную сортировку (например, Latin1_General_100_BIN2), каждая операция сортировки и сравнения будет применять весь спектр лингвистических правил, которые могут быть хорошо стоит при работе со строками, но при использовании кодов, но ненужно дорого, особенно при использовании типичных по умолчанию беззаботных коллайсов.

Некоторые «лучше» (но не идеально) решений будут:

  1. Если вам действительно нужны Unicode символов, по крайней мере указать двоичные сортировки, такие как Latin1_General_100_BIN2.
  2. Если вам не нужны символы Юникода, переключитесь на использование VARCHAR, который займет половину места и сортирует/сравнивает быстрее. Кроме того, по-прежнему используйте двоичную сортировку.

Лучше всего это:

  1. Добавить INT IDENTITY столбец в User таблицы, названный UseID
  2. Сделать UserID кластеризированного PK
  3. Добавить столбец INT (не IDENTITY) к Related стол, названный UserID
  4. Добавить FK от Related Назад к User по UserID
  5. Удалить столбец RelatedUser из таблицы Related.
  6. Добавить некластеризованный, уникальный индекс к User таблице на UserCode колонке (это делает его «альтернативный ключ»)
  7. падения и воссоздают UserIdInput пользовательского типа таблицы, чтобы иметь INT тип данных вместо NVARCHAR(100)
  8. Если это вообще возможно, изменить ID столбец таблицы User иметь двоичное объединение (т.е. Latin1_General_100_BIN2)
  9. Если возможно, переименовать текущий Id столбец в таблице User быть UserCode или S что-то вроде этого.
  10. Если пользователи вводят значения «Код» (что означает: не может гарантировать, что они всегда будут использовать весь верхний регистр или все нижние регистры), то лучше всего добавить триггер AFTER INSERT, UPDATE в таблицу User, чтобы убедиться, что значения всегда все в верхнем регистре (или все в нижнем регистре). Это также означает, что при поиске в «Кодексе» необходимо убедиться, что все входящие запросы используют одинаковые все верхние или все нижестоящие значения. Но эта небольшая часть дополнительной работы окупится.

Вся система поблагодарит вас и продемонстрирует вам свою признательность, будучи более эффективным :-).

Еще одна вещь, которую следует учитывать: TVP - это переменная таблицы, и по умолчанию они только когда-либо появляются оптимизатором запросов, чтобы иметь одну строку. Поэтому имеет смысл, что добавление нескольких тысяч записей в TVP замедлит его. Один трюк, который поможет ускорить TVP в этом сценарии, - это добавить OPTION (RECOMPILE) в запрос. Повторная компиляция запросов с помощью переменных таблицы заставит оптимизатор запросов видеть истинный подсчет строк. Если это никому не помогает, другой трюк заключается в том, чтобы сбрасывать переменную таблицы TVP в локальную временную таблицу (то есть #TempUserIDs), поскольку они поддерживают статистику и оптимизируют ее лучше, если в них содержится не более нескольких строк.

Из комментария OP по этому ответу:

[UID] является идентификатор, используемый по нашей системе (XXX-Y-ZZZZZZZZZZ ...), XXX, являющиеся буквами, Y означает число и Z являющиеся числами

Да, я полагал, что это идентификатор или код какого-то рода, так что это не меняет мой совет. NVARCHAR, особенно если используется не двоичное, нечувствительное к регистру сопоставление, вероятно, является одним из наихудших вариантов типа данных для этого значения. Этот идентификатор должен находиться в столбце с именем UserCode в таблице User с некластеризованным индексом, определенным на нем. Это делает его «альтернативным» ключом и быстрый и простой поиск с уровня приложения, один раз, чтобы получить «внутреннее» целочисленное значение для этой строки, столбец INT IDENTITY в качестве фактического UserID (обычно лучше всего назвать столбцы имен как {table_name} ID для согласованности/упрощения обслуживания с течением времени). Значение UserID INT - это то, что входит во все связанные таблицы как FK. Стол INT ПРИСОЕДИНЯЙТЕСЬ много быстрее, чем NVARCHAR. Даже используя двоичную сортировку, этот столбец NVARCHAR, будучи быстрее его текущей реализации, по-прежнему будет составлять не менее 32 байт (на основе приведенного примера XXX-Y-ZZZZZZZZZZ), тогда как INT будет всего 4 байта. И да, эти дополнительные 28 байтов do имеют значение, особенно если у вас 13 миллионов строк. Помните, что это не просто пространство на диске, которое эти значения занимают, это также память, поскольку ВСЕ данные, которые считываются для запросов, проходят через буферный пул (то есть физическую память!).

В этом случае, однако, мы не следуем внешним ключам в любом месте, а напрямую запрашиваем их. Если они индексируются, это имеет значение?

Да, это по-прежнему имеет значение, поскольку вы выполняете ту же операцию, что и JOIN: вы берете каждое значение в основной таблице и сравниваете его со значениями в переменной таблицы/TVP. Это по-прежнему не двоичное, нечувствительное к регистру (я предполагаю) сравнение, которое очень медленное по сравнению с двоичным сравнением. Каждая буква должна оцениваться не только в верхнем и нижнем регистре, но и во всех других кодах Юникода, которые могут быть приравнены к каждой букве (и есть больше, чем вы думаете, что будет соответствовать A - Z!). Индекс сделает его быстрее, чем не имеет индекса, но не так быстро, как сравнение простого простого значения, которое не имеет другого представления.

+0

Plus1 - Даже не заметил nvarchar –

+0

Хотя я вижу вашу точку зрения на NVARCHAR как внешние ключи, это идентификатор, используемый в нашей системе (XXX-Y-ZZZZZZZZZZ ...), XXX - буквы, Y - число и Z - числа. В этом случае, однако, мы не следуем внешним ключам в любом месте, а напрямую запрашиваем их. Если они индексируются, это имеет значение? – bech

+0

@srutzky Спасибо за ввод (и обновленное объяснение). В настоящее время я пытаюсь сделать это на копии базы данных, чтобы увидеть разницу. С чувством кишки, думаете ли вы, что мы можем получить те же результаты для 1-2 тысяч строк ввода, как мы можем сказать, 100, с этим изменением? (Я знаю, немного растянуть, чтобы догадаться об этих вещах, но все же ...) – bech

1

Основная проблема заключается в том, что она использует 63% усилия на сортировке .

ORDER BY CreationTime DESC 

Я хотел бы предложить и индекс CreationTime

Или попробуйте индекс по RelatedStory, CreationTime

+0

Привет. Да, я пробовал это, но он, похоже, не хотел использовать он, даже после того, как я добавил предложение where, используя индексированное поле, и перекомпилировал план выполнения. Любые мысли о том, почему? – bech

+0

Вы пробовали оба из них? И я пошел бы с соединением над существующим. – Paparazzi

+0

Я действительно только пробовал индекс только в поле CreationTime. Попробуем использовать как RelatedStory, так и CreationTime. – bech