2016-09-27 6 views
2

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

Основой для этих данных является следующее: компания отслеживает фондовые портфели для своих 100 клиентов. Каждый из 1000 акций имеет ежедневную запись, в которой перечислены четыре инвестора, которые владеют им, вместе с их процентом. К сожалению, у этого есть сбой, который позволяет владельцу появляться несколько раз. Процедура анализирует данные и отделяет записи таким образом, чтобы каждый день составлял 4 записи для каждой акции, а затем складывал общую сумму портфеля для каждого владельца. Однако, поскольку существует несколько записей, это может завышать значение для этого владельца. Таким образом, флаг вводится для идентификации любого из этих дубликатов. Позже в коде значение каждой строки умножается на этот флаг, который равен 0 для дубликата и 1, если нет.

У меня есть пять способов обновления этого флага. Я начинаю с 0, это просто использование CTE с оператором SELECT в качестве базовой линии; он занимает около 0,07 секунды. 1 использует CTE с JOIN для обновления таблицы и занимает около 48 секунд. 2 использует вложенный оператор select вместо CTE и занимает около 48 секунд. 3 сбрасывает CTE на переменную таблицы и присоединяется к ней и занимает около 0,13 секунды. 4 Я думал, что это будет наименее эффективным, потому что он использует контурный цикл и обновляет по одной строке за раз, но это заняло всего 0,17 секунды. 5 использует оператор CASE для обновления всех строк, присоединенных к CTE, и занимает около 48 секунд.

DECLARE @OwnRec TABLE (
     StockID   INT 
    , TradeDate   DATE 
    , Shares   DECIMAL(4,0) 
    , Price    DECIMAL(4,2) 
    , Owner1   INT 
    , Owner1Pct   DECIMAL(3,2) 
    , Owner2   INT 
    , Owner2Pct   DECIMAL(3,2) 
    , Owner3   INT 
    , Owner3Pct   DECIMAL(3,2) 
    , Owner4   INT 
    , Owner4Pct   DECIMAL(3,2) 
    ) 

DECLARE @OwnRec2 TABLE (
     RecID    INT IDENTITY 
    , StockID   INT 
    , TradeDate   DATE 
    , Shares   DECIMAL(4,0) 
    , Price    DECIMAL(4,2) 
    , Owner0   INT 
    , Owner0Pct   DECIMAL(3,2) 
    , OwnerNum   INT 
    , DupeOwner   TINYINT 
    ) 

DECLARE @CullDupe TABLE (
     ID    INT IDENTITY 
    , RecID    INT 
    ) 

DECLARE @Method  INT 
     , @Counter1  INT = 0 
     , @StartTime DATETIME 

--Populate tables with dummy data 
WHILE @Counter1 < 1000 
    BEGIN 
     SET @Counter1 += 1 
     INSERT INTO @OwnRec (
       StockID 
      , TradeDate 
      , Shares  
      , Price  
      , Owner1  
      , Owner1Pct 
      , Owner2  
      , Owner2Pct 
      , Owner3  
      , Owner3Pct 
      , Owner4  
      , Owner4Pct 
      ) 
     SELECT @Counter1 
      , '2016-09-26' 
      , ROUND((RAND() * 1000 + 500)/25,0)*25 
      , ROUND((RAND() * 30 + 20),2) 
      , ROUND((RAND() * 100 + .5),0) 
      , CAST(ROUND((RAND() * 5 + .5),0)*.05 AS DECIMAL(3,2)) 
      , ROUND((RAND() * 100 + .5),0) 
      , CAST(ROUND((RAND() * 5 + .5),0)*.05 AS DECIMAL(3,2)) 
      , ROUND((RAND() * 100 + .5),0) 
      , CAST(ROUND((RAND() * 5 + .5),0)*.05 AS DECIMAL(3,2)) 
      , ROUND((RAND() * 100 + .5),0) 
      , CAST(ROUND((RAND() * 5 + .5),0)*.05 AS DECIMAL(3,2)) 
    END 

SET @Counter1 = 0 

WHILE @Counter1 < 1000 
    BEGIN 
     SET @Counter1 += 1 
     INSERT INTO @OwnRec (
       StockID 
      , TradeDate 
      , Shares  
      , Price  
      , Owner1  
      , Owner1Pct 
      , Owner2  
      , Owner2Pct 
      , Owner3  
      , Owner3Pct 
      , Owner4  
      , Owner4Pct 
      ) 
     SELECT @Counter1 + 1000 
      , '2016-09-27' 
      , Shares 
      , ROUND(Price * ROUND(RAND()*10 + .5,0)*.01+.95,2) 
      , Owner1  
      , Owner1Pct 
      , Owner2  
      , Owner2Pct 
      , Owner3  
      , Owner3Pct 
      , Owner4  
      , Owner4Pct 
      FROM @OwnRec WHERE StockID = @Counter1 
    END 

UPDATE orx 
    SET Owner2Pct = Owner1Pct 
     FROM @OwnRec orx 
      WHERE Owner1 = Owner2 

UPDATE orx 
    SET Owner3Pct = Owner1Pct 
     FROM @OwnRec orx 
      WHERE Owner1 = Owner3 

UPDATE orx 
    SET Owner4Pct = Owner1Pct 
     FROM @OwnRec orx 
      WHERE Owner1 = Owner4 

UPDATE orx 
    SET Owner3Pct = Owner2Pct 
     FROM @OwnRec orx 
      WHERE Owner2 = Owner3 

UPDATE orx 
    SET Owner4Pct = Owner2Pct 
     FROM @OwnRec orx 
      WHERE Owner2 = Owner4 

UPDATE orx 
    SET Owner4Pct = Owner3Pct 
     FROM @OwnRec orx 
      WHERE Owner3 = Owner4 

INSERT INTO @OwnRec2 
    SELECT StockID, TradeDate, Shares, Price, Owner1 AS Owner0, Owner1Pct, 1, 1 AS Owner0Pct 
     FROM @OwnRec 
    UNION 
    SELECT StockID, TradeDate, Shares, Price, Owner2 AS Owner0, Owner2Pct, 2, 1 AS Owner0Pct 
     FROM @OwnRec 
    UNION 
    SELECT StockID, TradeDate, Shares, Price, Owner3 AS Owner0, Owner3Pct, 3, 1 AS Owner0Pct 
     FROM @OwnRec 
    UNION 
    SELECT StockID, TradeDate, Shares, Price, Owner4 AS Owner0, Owner4Pct, 4, 1 AS Owner0Pct 
     FROM @OwnRec 
--END Populate tables with dummy data 

SET @StartTime = GETDATE() 

SET @Method = 5 -- Choose which method to test 


--CASE 0: Just identify duplicates 

IF @Method = 0 
    BEGIN 
     ; WITH CullDupe 
      AS (
       SELECT RecID, ROW_NUMBER() OVER (PARTITION BY StockID, TradeDate, Owner0 ORDER BY OwnerNum) AS rn 
        FROM @OwnRec2 
       ) 
     SELECT * FROM CullDupe WHERE rn > 1 
    END 


--CASE 1: Update on JOIN to CTE 

IF @Method = 1 
    BEGIN 
     ; WITH CullDupe 
      AS (
       SELECT RecID, ROW_NUMBER() OVER (PARTITION BY StockID, TradeDate, Owner0 ORDER BY OwnerNum) AS rn 
        FROM @OwnRec2 
       ) 
     UPDATE OR2 
      SET DupeOwner = 0 
       FROM @OwnRec2 OR2 
        JOIN CullDupe cd 
         ON OR2.RecID = cd.RecID 
        WHERE rn > 1 
    END 


--CASE 2: Update on JOIN to nested SELECT 

IF @Method = 2 
    BEGIN 
     UPDATE OR2 
      SET DupeOwner = 0 
       FROM @OwnRec2 OR2 
        JOIN (SELECT RecID, ROW_NUMBER() OVER 
         (PARTITION BY StockID, TradeDate, Owner0 ORDER BY OwnerNum) AS rn 
         FROM @OwnRec2) cd 
         ON OR2.RecID = cd.RecID 
        WHERE rn > 1 
    END 


--CASE 3: Update on JOIN to temp table 

IF @Method = 3 
    BEGIN 
     ; WITH CullDupe 
      AS (
       SELECT RecID, ROW_NUMBER() OVER (PARTITION BY StockID, TradeDate, Owner0 ORDER BY OwnerNum) AS rn 
        FROM @OwnRec2 
       ) 

     INSERT INTO @CullDupe SELECT RecID FROM CullDupe WHERE rn > 1 

     UPDATE OR2 
      SET DupeOwner = 0 
       FROM @OwnRec2 OR2 
        JOIN @CullDupe cd 
         ON OR2.RecID = cd.RecID 
    END 


--CASE 4: Update using counted loop 

IF @Method = 4 
    BEGIN 
     ; WITH CullDupe 
      AS (
       SELECT RecID, ROW_NUMBER() OVER (PARTITION BY StockID, TradeDate, Owner0 ORDER BY OwnerNum) AS rn 
        FROM @OwnRec2 
       ) 

     INSERT INTO @CullDupe SELECT RecID FROM CullDupe WHERE rn > 1 
     SET @Counter1 = 0 
     WHILE @Counter1 < (SELECT MAX(ID) FROM @CullDupe) 
      BEGIN 
       SET @Counter1 += 1 
       UPDATE OR2 
        SET DupeOwner = 0 
         FROM @OwnRec2 OR2 
          WHERE RecID = (SELECT RecID FROM @CullDupe WHERE ID = @Counter1) 
      END 
    END 


--CASE 5: Update using JOIN to CTE, but updating all rows (CASE to identify) 

IF @Method = 5 
    BEGIN 
     ; WITH CullDupe 
      AS (
       SELECT RecID, ROW_NUMBER() OVER (PARTITION BY StockID, TradeDate, Owner0 ORDER BY OwnerNum) AS rn 
        FROM @OwnRec2 
       ) 

     UPDATE OR2 
      SET DupeOwner = CASE WHEN rn > 1 THEN 0 ELSE 1 END 
       FROM @OwnRec2 OR2 
        JOIN CullDupe cd 
         ON OR2.RecID = cd.RecID 
    END 

SELECT 'Method ' + CAST(@Method AS NVARCHAR(1)) + ': ' + CAST(DATEDIFF(ms,@StartTime,GETDATE()) AS NVARCHAR(10)) + ' milliseconds' 

ответ

2

Это распространенная проблема с переменными таблицы.

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

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

enter image description here

Он по-прежнему, тем не менее предполагается, что 1 строка будет выбрасываться из пустой таблицы, так как это минимальная оценка строки в большинстве случаев из листового оператора в плане выполнения. Под деревом внутри вложенных циклов выполняется один раз для каждой строки из таблицы вождения. Поскольку, по оценкам, это 1 строка, подсвеченное вспомогательное дерево ниже оценивается как однократно. Фактически, все подэлемент будет выполняться 8000 раз (включая дорогостоящие операторы сканирования и сортировки таблицы).

enter image description here

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

Общих решения для оценки одной строки должны добавить OPTION (RECOMPILE) на проблемные заявления, так что таблица кардинального во время выполнения оператора может быть принято во внимание, или использовать флаг трассировки 2453 (который может вызвать автоматические компиляции после изменения мощностного) или использования вместо этого используется таблица #temp (которая может запускать автоматические перекомпиляции и дополнительно извлекать выгоду из статистики столбцов)

Более подробную информацию об этом можно найти in my answer here.

+0

Это абсолютно невероятно. OPTION (RECOMPILE) взял меня с 48,136 мс до 13 мс.Спасибо, что нашли время, чтобы объяснить это. – DaveX

+0

Последующие меры. У меня был еще один шанс воспользоваться этим сегодня. 18 000 строк, без проблем, со всякими объединениями и всякими вещами. Но мне нужно было добавить еще одну пьесу, и это соединение заняло ее более двух минут. Сводит меня с ума. Я попробовал CTE, затем переменную таблицы, затем таблицу temp. Наконец я вернулся к этому сообщению и посмотрел на код, который я создал в результате. Я закончил создание моей исходной переменной таблицы, а затем выполнил обновление, используя другую переменную таблицы, используя OPTION (RECOMPILE). 6 секунд. 6 секунд! Я получил его до 55, и это было потрясающе, сравнительно. Но теперь 6! Вау. – DaveX