Общее число строк смещения SQL медленно с предложением IN - PullRequest
5 голосов
/ 05 ноября 2019

Я использую этот sql на основе другого ответа, однако при включении массива в пунктах дела сводятся к получению общего количества. Если я удаляю общее количество, тогда запрос занимает менее 1 секунды. Есть ли лучший, более эффективный способ получить общее количество строк? Ответы, которые я видел, основаны на запросах sql 2013

DECLARE 
    @PageSize INT = 10, 
    @PageNum  INT = 1;

WITH TempResult AS(
    SELECT ID, Name
    FROM Table
     Where ID in ( 1 ,2 3, 4, 5, 6, 7, 8, 9 ,10)
), TempCount AS (
    SELECT COUNT(*) AS MaxRows FROM TempResult
)
SELECT *
FROM TempResult, 
 TempCount    <----- this is what is slow. Removing this and the query is super fast
ORDER BY TempResult.Name
    OFFSET (@PageNum-1)*@PageSize ROWS
    FETCH NEXT @PageSize ROWS ONLY

Ответы [ 11 ]

4 голосов
/ 11 ноября 2019

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

Я собираюсь предположить, что у вас есть куча, ~ 10M строк (12 872 738 для меня):

DECLARE @MaxRowCount bigint = 10000000,
        @Offset      bigint = 0;

DROP TABLE IF EXISTS #ExampleTable;
CREATE TABLE #ExampleTable
(
  ID   bigint      NOT NULL,
  Name varchar(50) COLLATE DATABASE_DEFAULT NOT NULL
);

WHILE @Offset < @MaxRowCount
BEGIN
  INSERT INTO #ExampleTable
  ( ID, Name )
    SELECT ROW_NUMBER() OVER ( ORDER BY ( SELECT NULL )),
           ROW_NUMBER() OVER ( ORDER BY ( SELECT NULL ))
      FROM master.dbo.spt_values SV
        CROSS APPLY master.dbo.spt_values SV2;
  SET @Offset = @Offset + ROWCOUNT_BIG();
END;

Если я выполню запрос, предоставленный через #ExampleTable, это займет около 4 секунд и даст мне план запроса:

Baseline query plan

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

Давайте дадим огромное количество предметов в нашем списке IN (5000 предметов от 1-5000). Компиляция плана заняла 4 секунды:

Large IN list query plan

Я могу получить свой номер до 15000 элементов, прежде чем обработчик запросов перестанет его обрабатывать, безизменение в плане запроса (компиляция занимает всего 6 секунд). Выполнение обоих запросов занимает около 5 секунд на моем компьютере.

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

Давайте посмотрим на некоторые альтернативы. Возможно, мы можем сделать некоторые из них в комбинации.

  1. Мы могли бы кэшировать список IN во временной таблице или табличной переменной.
  2. Мы могли бы использовать оконную функцию для вычисления количества
  3. Мы могли бы кэшировать наш CTE во временной таблице или табличной переменной
  4. Если на достаточно высокой версии SQL Server,используйте пакетный режим
  5. Измените индексы на своей таблице, чтобы сделать это быстрее.

Особенности рабочего процесса

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

Если нам нужен OLTP, то параметры кэширования, вероятно, не обсуждаются. Временные таблицы всегда будут принудительно перекомпилироваться, а переменные таблиц в запросах, которые основаны на правильной оценке, требуют принудительной перекомпиляции. Альтернатива может состоять в том, чтобы другая часть вашего приложения поддерживала постоянную таблицу с разбивкой по страницам или фильтрами (или обоими), а затем объединяла этот запрос с этим.

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

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

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

Независимо от рабочего процесса,оконные функции (особенно с индексами и / или пакетным режимом), вероятно, будут лучше.

Пакетный режим и оценщик мощности по умолчанию

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

Если вы не можете обновить свою базу данных, чтобы использовать новый кардинальный оценщик оптом, вам нужно включить его дляваш конкретный запрос. Для этого вы можете использовать следующую подсказку: OPTION( USE HINT( 'FORCE_DEFAULT_CARDINALITY_ESTIMATION' ) ), чтобы получить первый. Во-вторых, добавьте объединение в CCI (не нужно возвращать данные): LEFT OUTER JOIN dbo.EmptyCciForRowstoreBatchmode ON 1 = 0 - это позволяет SQL Server выбирать оптимизации в пакетном режиме. Эти рекомендации предполагают достаточно новую версию SQL Server.

Что такое CCI, не имеет значения;нам нравится держать пустой для согласованности, который выглядит следующим образом:

CREATE TABLE dbo.EmptyCciForRowstoreBatchmode
(
  __zzDoNotUse int NULL,
  INDEX CCI CLUSTERED COLUMNSTORE
);

Лучший план, который я мог получить, не изменяя таблицу, состоял в том, чтобы использовать оба из них. С теми же данными, что и раньше, это выполняется в <1 с. </p>

Batch Mode and NCE

WITH TempResult AS
(
  SELECT ID,
         Name,
         COUNT( * ) OVER ( ) MaxRows
    FROM #ExampleTable
    WHERE ID IN ( <<really long LIST>> )
)
  SELECT TempResult.ID,
         TempResult.Name,
         TempResult.MaxRows
    FROM TempResult
      LEFT OUTER JOIN dbo.EmptyCciForRowstoreBatchmode ON 1 = 0
    ORDER BY TempResult.Name OFFSET ( @PageNum - 1 ) * @PageSize ROWS FETCH NEXT @PageSize ROWS ONLY
    OPTION( USE HINT( 'FORCE_DEFAULT_CARDINALITY_ESTIMATION' ) );
4 голосов
/ 11 ноября 2019

Насколько я знаю, есть 3 способа добиться этого, кроме использования уже упомянутого подхода к таблице #temp. В приведенных ниже тестовых примерах я использовал экземпляр SQL Server 2016 Developer с оперативной памятью 6CPU / 16 ГБ и простую таблицу, содержащую ~ 25 млн строк.

Метод 1: CROSS JOIN

DECLARE
  @PageSize INT = 10
, @PageNum  INT = 1;

WITH TempResult AS (SELECT
                          id
                        , shortDesc
                    FROM  dbo.TestName
                    WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
SELECT
           *, MaxRows
FROM       TempResult
CROSS JOIN (SELECT COUNT(1) AS MaxRows FROM TempResult) AS TheCount
ORDER BY   TempResult.shortDesc OFFSET (@PageNum - 1) * @PageSize ROWS 
FETCH NEXT @PageSize ROWS ONLY;

Результат теста 1:

enter image description here

Метод 2: COUNT (*) OVER ()

DECLARE
  @PageSize INT = 10
, @PageNum  INT = 1;

WITH TempResult AS (SELECT
                          id
                        , shortDesc
                    FROM  dbo.TestName
                    WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
)
SELECT
         *, MaxRows = COUNT(*) OVER()
FROM     TempResult
ORDER BY TempResult.shortDesc OFFSET (@PageNum - 1) * @PageSize ROWS
FETCH NEXT @PageSize ROWS ONLY;

Результат теста 2:

enter image description here

Метод 3: 2-й CTE

Тестрезультат 3 (использованный T-SQL был таким же, как в вопросе):

enter image description here

Заключение

Самый быстрый метод зависит от вашей структуры данных (и общего количества строк) в сочетании с размером / нагрузкой вашего сервера. В моем случае использование COUNT (*) OVER () оказалось самым быстрым методом. Чтобы найти то, что лучше для вас, вы должны проверить, что лучше для вашего сценария. И пока не исключайте этот #table подход; -)

1 голос
/ 13 ноября 2019

Вы можете попытаться посчитать строки , пока фильтрует таблицу, используя ROW_NUMBER():

DECLARE 
    @PageSize INT = 10, 
    @PageNum  INT = 1;

;WITH 
TempResult AS (
    SELECT ID, Name, ROW_NUMBER() OVER (ORDER BY ID) N
    FROM Table
    Where ID in ( 1 ,2 3, 4, 5, 6, 7, 8, 9 ,10)
), 
TempCount AS (
    SELECT TOP 1 N AS MaxRows 
    FROM TempResult
    ORDER BY ID DESC
)
SELECT *
FROM 
    TempResult, 
    TempCount 
ORDER BY 
    TempResult.Name
    OFFSET (@PageNum-1)*@PageSize ROWS
    FETCH NEXT @PageSize ROWS ONLY
1 голос
/ 09 ноября 2019

Оператор IN является печально известным препятствием для механизма запросов SQL Server. Когда он становится «массивным» (ваши слова), он замедляет даже простые запросы. По моему опыту, IN операторы с более чем 5000 элементов почти всегда неприемлемо замедляют любой запрос.

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

DECLARE @ids TABLE (ID int primary key );

-- This must be done in chunks of 1000
INSERT @ids (ID) VALUES
(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),...
...

;WITH TempResult AS
(
    SELECT tbl.ID, tbl.Name
    FROM Table tbl
    JOIN @ids ids ON ids.ID = tbl.ID
),
TempCount AS 
(
    SELECT COUNT(*) AS MaxRows FROM TempResult
)
SELECT *
FROM TempResult, 
     TempCount
ORDER BY TempResult.Name
    OFFSET (@PageNum-1)*@PageSize ROWS
    FETCH NEXT @PageSize ROWS ONLY
1 голос
/ 09 ноября 2019

Это может быть выстрел в темноте, но вы можете попробовать использовать временную таблицу вместо cte . Хотя результаты производительности и предпочтение одного над другим зависит от варианта использования к варианту использования, временная таблица 1006 * иногда может на самом деле оказаться лучше, поскольку она позволяет использовать индексы и выделенную статистику.

INSERT INTO #TempResult 
    SELECT ID, Name
    FROM Table
    WHERE ID in ( 1 ,2 3, 4, 5, 6, 7, 8, 9 ,10)
1 голос
/ 05 ноября 2019

Вы можете попытаться сформулировать это следующим образом:

WITH TempResult AS(
      SELECT ID, Name, COUNT(*) OVER () as maxrows
      FROM Table
      Where ID in ( 1 ,2 3, 4, 5, 6, 7, 8, 9 ,10)
     )

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

0 голосов
/ 14 ноября 2019

Я не могу проверить это в данный момент, но при взгляде через него меня поразило, что указание умножения (перекрестное соединение), как в:

FROM TempResult, 
 TempCount    <----- this is what is slow. Removing this and the query is super 

, может быть проблемой

Какон выполняет, когда написано просто как:

DECLARE 
    @PageSize INT = 10, 
    @PageNum  INT = 1;

WITH TempResult AS(
    SELECT ID, Name
    FROM Table
     Where ID in ( 1 ,2 3, 4, 5, 6, 7, 8, 9 ,10)
)
SELECT *, (SELECT COUNT(*) FROM TempResult) AS MaxRows
FROM TempResult
ORDER BY TempResult.Name
    OFFSET (@PageNum-1)*@PageSize ROWS
    FETCH NEXT @PageSize ROWS ONLY
0 голосов
/ 14 ноября 2019

В прошлом я сталкивался с той же проблемой: число записей в CTE вызывает проблемы с производительностью при увеличении записей в таблицах подчеркивания.

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

Я изменил реализацию, чтобы улучшить время отклика. Я сделал следующее -

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

0 голосов
/ 14 ноября 2019

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

Лучшая практика, которую я видел до сих пор, состоит в том, чтобы разделить вычисления total и page выборки в двух отдельных SQL-запросах, а затем объединить их в обратном направлении в бэкэнд-сервисе.

поэтому возникает вопрос: почему вы намеревались сделать это таким образом? почему бы не сделать это в два прохода? В чем выгода, полученная за один проход?

0 голосов
/ 13 ноября 2019

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

DECLARE 
    @PageSize INT = 10, 
    @PageNum  INT = 1,
    @MaxRows bigint = (SELECT COUNT(1) FROM Table Where ID in ( 1 ,2 3, 4, 5, 6, 7, 8, 9 ,10));

WITH TempResult AS(
    SELECT ID, Name
    FROM Table
     Where ID in ( 1 ,2 3, 4, 5, 6, 7, 8, 9 ,10)
)
SELECT *
FROM TempResult, 
 @MaxRows TempCount    <----- this is what is slow. Removing this and the query is super fast
ORDER BY TempResult.Name
    OFFSET (@PageNum-1)*@PageSize ROWS
    FETCH NEXT @PageSize ROWS ONLY
...