2012-05-25 3 views
16

У меня вопрос производительности для общих табличных выражений в SQL Server. В нашей команде разработчиков мы используем много цепей CTE при построении наших запросов. В настоящее время я работаю над запросом, который имел ужасную производительность. Но я узнал, что если я в середине цепи вставил все записи до этого CTE во временную таблицу, а затем продолжил, но выбрав из этой таблицы темп, я значительно улучшил производительность. Теперь я хотел бы получить некоторую помощь, чтобы понять, относится ли этот тип изменений только к этому конкретному запросу и почему два случая, которые вы увидите ниже, значительно отличаются по производительности. Или, возможно, мы можем злоупотреблять CTE в нашей команде, и можем ли мы получить производительность в целом, узнав об этом случае?Плохая производительность T-SQL с CTE

Пожалуйста, попробуйте объяснить мне именно то, что здесь происходит ...

код завершения, и вы сможете запустить его на SQL Server 2008 и, возможно, 2005 тоже. Одна часть закомментирована, и моя идея состоит в том, что вы можете переключать два случая, комментируя один или другой. Вы можете увидеть, где разместить комментарии к блоку, я отметил эти места --block comment here и --end block comment here

Это медленный исполняемый регистр, который был раскомментирован по умолчанию. Здесь вы:

--Declare tables to use in example. 
CREATE TABLE #Preparation 
(
    Date DATETIME NOT NULL 
    ,Hour INT NOT NULL 
    ,Sales NUMERIC(9,2) 
    ,Items INT 
); 

CREATE TABLE #Calendar 
(
    Date DATETIME NOT NULL 
) 

CREATE TABLE #OpenHours 
(
    Day INT NOT NULL, 
    OpenFrom TIME NOT NULL, 
    OpenTo TIME NOT NULL 
); 

--Fill tables with sample data. 
INSERT INTO #OpenHours (Day, OpenFrom, OpenTo) 
VALUES 
    (1, '10:00', '20:00'), 
    (2, '10:00', '20:00'), 
    (3, '10:00', '20:00'), 
    (4, '10:00', '20:00'), 
    (5, '10:00', '20:00'), 
    (6, '10:00', '20:00'), 
    (7, '10:00', '20:00') 

DECLARE @CounterDay INT = 0, @CounterHour INT = 0, @Sales NUMERIC(9, 2), @Items INT; 

WHILE @CounterDay < 365 
BEGIN 
    SET @CounterHour = 0; 
    WHILE @CounterHour < 5 
    BEGIN 
     SET @Items = CAST(RAND() * 100 AS INT); 
     SET @Sales = CAST(RAND() * 1000 AS NUMERIC(9, 2)); 
     IF @Items % 2 = 0 
     BEGIN 
      SET @Items = NULL; 
      SET @Sales = NULL; 
     END 

     INSERT INTO #Preparation (Date, Hour, Items, Sales) 
     VALUES (DATEADD(DAY, @CounterDay, '2011-01-01'), @CounterHour + 13, @Items, @Sales); 

     SET @CounterHour += 1; 
    END 
    INSERT INTO #Calendar (Date) VALUES (DATEADD(DAY, @CounterDay, '2011-01-01')); 
    SET @CounterDay += 1; 
END 

--Here the query starts. 
;WITH P AS (
    SELECT DATEADD(HOUR, Hour, Date) AS Hour 
     ,Sales 
     ,Items 
    FROM #Preparation 
), 
O AS (
     SELECT DISTINCT DATEADD(HOUR, SV.number, C.Date) AS Hour 
     FROM #OpenHours AS O 
      JOIN #Calendar AS C ON O.Day = DATEPART(WEEKDAY, C.Date) 
      JOIN master.dbo.spt_values AS SV ON SV.number BETWEEN DATEPART(HOUR, O.OpenFrom) AND DATEPART(HOUR, O.OpenTo) 
), 
S AS (
    SELECT O.Hour, P.Sales, P.Items 
    FROM O 
     LEFT JOIN P ON P.Hour = O.Hour 
) 

--block comment here case 1 (slow performing) 
--With this technique it takes about 34 seconds. 
,N AS (
     SELECT 
      A.Hour 
      ,A.Sales AS SalesOrg 
      ,CASE WHEN COALESCE(B.Sales, C.Sales, 1) < 0 
       THEN 0 ELSE COALESCE(B.Sales, C.Sales, 1) END AS Sales 
      ,A.Items AS ItemsOrg 
      ,COALESCE(B.Items, C.Items, 1) AS Items 
     FROM S AS A 
     OUTER APPLY (SELECT TOP 1 * 
        FROM S 
        WHERE Hour <= A.Hour 
         AND Sales IS NOT NULL 
         AND DATEDIFF(DAY, Hour, A.Hour) = 0      
        ORDER BY Hour DESC) B 
     OUTER APPLY (SELECT TOP 1 * 
        FROM S 
        WHERE Sales IS NOT NULL 
         AND DATEDIFF(DAY, Hour, A.Hour) = 0 
        ORDER BY Hour) C 
    ) 
--end block comment here case 1 (slow performing) 

/*--block comment here case 2 (fast performing) 
--With this technique it takes about 2 seconds. 
SELECT * INTO #tmpS FROM S; 

WITH 
N AS (
     SELECT 
      A.Hour 
      ,A.Sales AS SalesOrg 
      ,CASE WHEN COALESCE(B.Sales, C.Sales, 1) < 0 
       THEN 0 ELSE COALESCE(B.Sales, C.Sales, 1) END AS Sales 
      ,A.Items AS ItemsOrg 
      ,COALESCE(B.Items, C.Items, 1) AS Items 
     FROM #tmpS AS A 
     OUTER APPLY (SELECT TOP 1 * 
        FROM #tmpS 
        WHERE Hour <= A.Hour 
         AND Sales IS NOT NULL 
         AND DATEDIFF(DAY, Hour, A.Hour) = 0      
        ORDER BY Hour DESC) B 
     OUTER APPLY (SELECT TOP 1 * 
        FROM #tmpS 
        WHERE Sales IS NOT NULL 
         AND DATEDIFF(DAY, Hour, A.Hour) = 0 
        ORDER BY Hour) C 
    ) 
--end block comment here case 2 (fast performing)*/ 
SELECT * FROM N ORDER BY Hour 


IF OBJECT_ID('tempdb..#tmpS') IS NOT NULL DROP TABLE #tmpS; 

DROP TABLE #Preparation; 
DROP TABLE #Calendar; 
DROP TABLE #OpenHours; 

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

Для меня случай 1 занимает около 34 секунд, а корпус 2 занимает около 2 секунд. Разница в том, что я сохраняю результат из S в таблице temp в случае 2, в случае 1 я использую S непосредственно в следующем CTE.

+1

+1 Для исполняемого кода. Я предлагаю запустить их оба и вставить XML-план выполнения в SQL Sentry Plan Explorer. Причина разницы будет очевидна. Он заканчивает сканирование '# Preparation' 20 000 раз в одной части плана и 10 000 раз в другой части для примера. –

+0

Спасибо. Я установил Explorer SQL Sentry Plan Explorer. Я все еще изучаю SQL Server, и я не могу читать планы выполнения, ни в Sentry Plan. Но ответ заключается в том, что нельзя сказать ничего о том, если он лучше всего подходит для CTE или temp-таблицы в определенных сценариях, не исследуя планы выполнения? Где вы видите 20 000 и 10000 раз? Я не могу найти его. Можно ли сказать что-то о том, почему одна часть сканирует # Подготовка 20 000 раз, обращаясь к коду? – John

+0

Вы видели это? «Таблица SQL 2005 CTE vs TEMP Производительность при использовании в объединениях других таблиц» http://stackoverflow.com/questions/1531835/sql-2005-cte-vs-temp-table-performance-when-used-in-joins- из-за-столов –

ответ

5

CTE - это просто ярлык синтаксиса. Этот CTE запускается (и повторно запускается) в соединении. С #temp он получает оценку один раз, а затем результаты повторно используются в соединении.

Документация является вводящей в заблуждение.

MSDN_CTE

Выражение общей таблицы (КТР) можно рассматривать в качестве временного набора результатов.

Эта статья объясняет, что лучше

PapaCTEarticle

КТР это хорошо подходит для этого типа сценария, так как это делает T-SQL гораздо более удобным для чтения (например, вид), но он может быть использован более одного раза в запросе, который сразу же следует в той же партии. Конечно, он недоступен за пределами этого объема. Кроме того, CTE представляет собой конструкцию на уровне языка, что означает, что SQL Server не создает внутреннюю или временную таблицы. Основной запрос CTE будет вызываться каждый раз, когда он ссылается в следующем запросе.

Посмотрите на таблицу значений параметров

TVP

Они имеют структуру вроде #temp, но не так много накладных расходов. Они доступны только для чтения, но, похоже, вам нужно только чтение. Создание и отбрасывание #temp будет меняться, но на сервере с низким и средним значением это 0,1-секундный удар, а с TVP существенно не пострадали.

+0

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

+0

@JNK Помогите мне. TVP читается только так, почему мне все равно, если его нельзя отменить. Этот образец не выбирает только вставки, обновления или удаления. Где риск? Вы помогли мне не раз и всегда были верны. – Paparazzi

+1

У меня есть общее недоверие к ТВП для чего угодно, кроме вывода или тестирования. Они по-прежнему записывают в файл журнала и имеют служебные данные ввода-вывода на диске, но на самом деле не являются атомарными и не могут быть проиндексированы, не имеют статистики, а планы exec всегда предполагают одну строку для них. – JNK

11

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

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

Ваш первый (медленный) случай требует, чтобы функции даты выполнялись для каждой строки.

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

Это, как правило, не так заметно, если вы не выполняете какую-либо логику в производном от функции поле. В вашем случае вы делаете ORDER BY на Hour, что очень дорого. Во втором примере это простой вид в поле, но в первом вы используете эту функцию для каждой строки, затем сортировку.

Для более углубленного изучения CTE, см. this question on DBA.SE.

Смежные вопросы