Как повысить производительность по запросу из одной таблицы на сервере MS SQL - PullRequest
2 голосов
/ 20 марта 2020

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

У меня есть таблица шириной примерно в 35 столбцов, со стандартным ассортиментом столбцов (несколько int, куча varchar () размеров от 10 до 255, довольно базовый c), в котором я поместил кластерный индекс в столбец, давайте назовем «PackageID» для ради объяснения. В этой таблице немного севернее миллиона записей, так что есть большой объем данных, которые нужно пролистать, и может быть одна или несколько записей с одним и тем же PackageID из-за характера записей, но это всего лишь один «плоский» Таблица.

Попадание в таблицу У меня есть хранимая процедура, которая принимает аргумент varchar(max), который может быть единственным PackageID или это может быть список с разделителями-запятыми 10, 50, 500 или более. SPro c вызывает довольно стандартную простую функцию Split() (найденную здесь и на других сайтах), которая разбивает список, возвращая значения в виде таблицы, которую я затем пытаюсь отфильтровать по таблице для результатов. Идентификаторы представляют собой целые значения в настоящее время длиной до 5 цифр, в будущем они будут расти, но сейчас только 5.

Я пробовал несколько вариантов запроса внутри SPro c (только запрос здесь для краткости):

SELECT PackageID, Column01, Column02, Column03, ... , ColumnN
FROM MyTable
WHERE PackageID IN (SELECT SplitValue FROM dbo.Split(@ListOfIDs, ','))

и

;WITH cteIDs AS (
    SELECT SplitValue 
    FROM dbo.Split(@ListOfIDs, ',')
)
SELECT PackageID, Column01, Column02, Column03, ... , ColumnN
FROM MyTable m
INNER JOIN cteIDs c ON m.PackageID = c.SplitValue

Запуск из SSMS в обоих оценочных планах выполнения выглядит одинаково и занимает примерно одинаковое количество времени. Когда @ListOfIDs короткий, записи возвращаются быстро, но по мере того, как список идентификаторов увеличивается (и может достигать сотен и более), время выполнения может go минут или дольше. Здесь нет триггеров, ничто другое не использует его, запрос не блокируется и не блокируется ничем, что я могу сказать ... он просто работает медленно.

Я чувствую, что здесь упускаю что-то безумно простое но я просто не вижу этого.

Спасибо за любую помощь, спасибо!

ОБНОВЛЕНИЕ

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

CREATE FUNCTION [dbo].[Split] 
( 
        @String VARCHAR(max), 
        @Delimiter VARCHAR(5) 
) 
RETURNS @SplittedValues TABLE 
( 
  OccurenceId SMALLINT IDENTITY(1,1), 
  SplitValue VARCHAR(max) 
) 
AS 
BEGIN 
    DECLARE @SplitLength INT 

    WHILE LEN(@String) > 0 
    BEGIN 
        SELECT @SplitLength = (CASE CHARINDEX(@Delimiter, @String) 
                                    WHEN 0 THEN LEN(@String) 
                                    ELSE CHARINDEX(@Delimiter, @String) -1 
                                END)

        INSERT INTO @SplittedValues 
        SELECT SUBSTRING(@String, 1, @SplitLength)

        SELECT @String = (CASE (LEN(@String) - @SplitLength) 
                                WHEN 0 THEN '' 
                                ELSE RIGHT(@String, LEN(@String) - @SplitLength - 1) 
                          END) 
    END 
    RETURN 
END 

GO

ОБНОВЛЕНИЕ - Тестирование предложений комментариев

Я попытался опробовать предложения в комментариях, и вот что я обнаружил ...

  • Размер таблицы: 1 081 154 записей
  • Уникальный счетчик "PackageID" : 16008 ID
  • Размер теста списка: 500 случайных идентификаторов (ввод аргументов строки через запятую)

Когда я запускаю (в SSMS) запрос, используя только функцию Split(), он принимает в среднем 309 секунд, чтобы вернуть 373 761 запись.

Когда я запускаю запрос, но сначала выкидываю результаты Split() в таблицу @TempTable (с индексом первичного ключа) и соединяю ее с таблицей, это занимает в среднем 111 секунд, чтобы вернуть те же 373 761 записей.

Я понимаю, что это много записей, но это плоская таблица с кластеризованным индексом на PackageID. Запрос является очень простым c, просто запрашивая совпадения записей по идентификатору. Здесь нет ни вычислений, ни обработки, ни других соединений с другими таблицами, операторов CASE, группировок, имущих и т. Д. c. Я не понимаю, почему выполнение запроса занимает так много времени. Я видел, как другие запросы с огромной логикой c возвращали тысячи записей в секунду, почему эта «простая» вещь застряла?

ОБНОВЛЕНИЕ - Добавление Exe c Plan

В соответствии с запросом приведен план выполнения запроса, который я выполняю. После выгрузки разделенных значений входящего списка идентификаторов с разделителями в @TempTable запрос просто запрашивает все записи из таблицы A («MyTable») с совпадающими идентификаторами, найденными в таблице B (@TempTable). Вот и все.

Execution Plan for Query

Обновление - Заказ по

В прилагаемом Плане выполнения, отмеченном в комментариях, присутствовал ORDER BY, который, по-видимому, потреблял значительную сумму накладных расходов. Я удалил это из своего запроса и повторно запустил свои тесты, что привело к минимальному улучшению времени выполнения. На тестовом прогоне, который ранее занимал 7 минут, без ORDER BY будет завершено в 6:30 до 6:45 минут.

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

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

Ответы [ 2 ]

0 голосов
/ 20 марта 2020

Это не большая часть ответа, а скорее список сегментов. Я не вижу очевидной причины, почему этот запрос работает плохо. Ниже приведены некоторые маловероятные, действительно маловероятные и нелепые возможности.

+ 1 для «убедитесь, что типы данных с обеих сторон объединения идентичны»

+ 1 при загрузке «разделенных» данных в свою временную таблицу.

Я рекомендую таблицу #temp, созданную с первичным ключом (в отличие от @temp), по непонятным причинам, связанным со статистикой, которая, как я считаю, перестала быть актуальной в более поздних версиях * Сервер 1020 * (я начал с 7.0 и легко теряю информацию о том, когда было добавлено множество новых новомодных).

Что показывает план запроса?

Попробуйте запустить его с «установить статистику io на ”, Чтобы увидеть, сколько операций чтения страниц действительно задействовано.

Во время тестирования вы уверены, что это ЕДИНСТВЕННЫЙ запрос, выполняющийся к этой базе данных?

« MyTable »- это таблица, верно? Не вид, синоним, чудовищность связанного сервера или другая причудливая конструкция?

Установлены ли какие-либо сторонние инструменты, которые могли бы регистрировать каждое ваше действие на БД и / или сервере?

Дайте это PackageId не является уникальным в MyTable, сколько данных на самом деле возвращается? Вполне может случиться так, что считывание данных и их передача обратно в вызывающую систему займет так много времени, хотя это действительно кажется маловероятным, если сервер не будет заполнен другой работой.

0 голосов
/ 20 марта 2020

Предположим, что вы не попадаете в ловушку использования различных типов данных для поиска по индексу вашей главной таблицы (т. Е. Ваш PackageID - это varchar, но не nvarchar или цифра c), тогда само присоединение к вашей таблице происходит очень быстро.

Чтобы подтвердить это, вы можете разделить процесс на 2 этапа, вставить в временную таблицу, а затем использовать временную таблицу для объединения. Если первый шаг очень медленный, а второй - супер быстрый, то это подтверждает мое предположение, приведенное выше.

Если шаг 1 медленный, это означает, что основная причина медленной производительности - это разделение, которое использует много вызовов подстроки. Предположим, в вашем списке 10000 элементов по 20 байт для каждого идентификатора. Это означает, что у вас есть переменная 200 КБ. При текущем вызове SUBSTRING он всегда копирует 200 КБ в новую строку на каждой итерации. Строка будет постепенно уменьшаться с 200 КБ до 0 КБ, но вы уже скопировали строку 100+ КБ 5000 раз. Это всего 1000 МБ потока данных.

Ниже приведены 3 функции. [Split $ 60769735 $ 0] - ваша оригинальная функция [Split $ 60769735 $ 1] использует XML [Split $ 60769735 $ 2] использует двоичное разделение, но также использует вашу оригинальную функцию

[Split $ 60769735 $ 1] быстро, потому что она использует специализированный синтаксический анализатор для XML, который уже может очень хорошо обрабатывать разбиение. [Разделить $ 60769735 $ 2] быстро, потому что оно меняет вашу сложность O (n ^ 2) на O (n log n)

Время выполнения: [Split $ 60769735 $ 0] = от 3 до 4 минут [Split $ 60769735 $ 1] = 2 секунды [Split $ 60769735 $ 2] = 7 секунд

ПРИМЕЧАНИЕ: поскольку это для демонстрационной цели, некоторые крайние случаи еще не обработаны. 1. Для [Split $ 60769735 $ 1], если значения могут содержать <> &, требуется некоторое экранирование 2. Для [Split $ 60769735 $ 2], если разделитель не может быть найден во второй половине строки (т. Е. Один дочерний элемент может быть длиной до 5000 символов), вам необходимо обработать случай, когда функция charindex не возвращает хит.

CREATE SCHEMA [TRY]
GO

CREATE FUNCTION [TRY].[Split$60769735$0]
( 
        @String VARCHAR(max), 
        @Delimiter VARCHAR(5) 
) 
RETURNS @SplittedValues TABLE 
( 
  OccurenceId INT IDENTITY(1,1), 
  SplitValue VARCHAR(max) 
) 
AS 
BEGIN 
    DECLARE @SplitLength INT 

    WHILE LEN(@String) > 0 
    BEGIN 
        SELECT @SplitLength = (CASE CHARINDEX(@Delimiter, @String) 
                                    WHEN 0 THEN LEN(@String) 
                                    ELSE CHARINDEX(@Delimiter, @String) -1 
                                END)

        INSERT INTO @SplittedValues 
        SELECT SUBSTRING(@String, 1, @SplitLength)

        SELECT @String = (CASE (LEN(@String) - @SplitLength) 
                                WHEN 0 THEN '' 
                                ELSE RIGHT(@String, LEN(@String) - @SplitLength - 1) 
                          END) 
    END 
    RETURN 
END
GO
CREATE FUNCTION [TRY].[Split$60769735$1]
( 
        @String VARCHAR(max), 
        @Delimiter VARCHAR(5) 
) 
RETURNS @SplittedValues TABLE 
( 
  OccurenceId INT IDENTITY(1,1), 
  SplitValue VARCHAR(max) 
) 
AS 
BEGIN 
    DECLARE @x XML = cast('<i>'+replace(@String,@Delimiter,'</i><i>')+'</i>' AS XML)
    INSERT INTO @SplittedValues 
    SELECT v.value('.','varchar(100)') FROM @x.nodes('i') AS x(v)
    RETURN 
END
GO
CREATE FUNCTION [TRY].[Split$60769735$2]
( 
        @String VARCHAR(max), 
        @Delimiter VARCHAR(5) 
) 
RETURNS @SplittedValues TABLE 
( 
  OccurenceId INT IDENTITY(1,1), 
  SplitValue VARCHAR(max) 
) 
AS 
BEGIN 
    DECLARE @len int = len(@String);
    IF @len > 10000 BEGIN
        DECLARE @mid int = charindex(@Delimiter,@String,@len/2);
        INSERT INTO @SplittedValues
        SELECT SplitValue FROM TRY.[Split$60769735$2](substring(@String, 1, @mid-1), @Delimiter);
        INSERT INTO @SplittedValues
        SELECT SplitValue FROM TRY.[Split$60769735$2](substring(@String, @mid+len(@Delimiter), @len-@mid-len(@Delimiter)+1), @Delimiter);
    END ELSE BEGIN
        INSERT INTO @SplittedValues
        SELECT SplitValue FROM TRY.[Split$60769735$0](@String, @Delimiter);
    END
    RETURN
END 
GO

ПРИМЕЧАНИЕ: - начиная с SQL Server 2016, будет встроенная функция разделения. Но, к сожалению, вы находитесь в 2012 году

Если шаг 1 быстрый, но шаг 2 медленный, возможны проблемы с несоответствием типа данных или отсутствующим индексом. В таком случае, публикация того, на что похож ваш план выполнения, поможет больше всего.

...