Как сделать курсор быстрее

Я написал этот курсор для отчета комиссии. Что происходит, так это то, что комиссия поступает в одну таблицу, записи - в другую таблицу. Я сопоставляю два на основе определенных критериев (точное совпадение недоступно). Проблема в том, что там, где существуют записи, есть дубликаты. Когда я сопоставляю комиссию с таблицей records, это может привести к получению этих дубликатов. Таким образом, представитель получает больше. С другой стороны, в таблице комиссий также есть дубликаты, но они действительны, потому что они просто означают, что счет был оплачен за 2 месяца.

Я написал этот запрос, но его выполнение занимает более 5 минут. У меня 50 000 записей в таблице записей и 100 000 в таблице комиссий. Есть ли способ улучшить этот курсор?

/* just preparation of cursor, this is not time consuming */
CREATE TABLE #result
  (
     repid         INT,
     AccountNo     VARCHAR(100),
     supplier      VARCHAR(15),
     CompanyName   VARCHAR(200),
     StartDate     DATETIME,
     EndDate       DATETIME,
     Product       VARCHAR(25),
     commodity     VARCHAR(25),
     ContractEnd   DATETIME,
     EstUsage      INT,
     EnrollStatus  VARCHAR(10),
     EnrollDate    DATETIME,
     ActualEndDate DATETIME,
     MeterStart    DATETIME,
     MeterEnd      DATETIME,
     ActualUsage   INT
  )

DECLARE @AccountNo VARCHAR(100)
DECLARE @supplier VARCHAR(10)
DECLARE @commodity VARCHAR(15)
DECLARE @meterstart DATETIME
DECLARE @meterEnd DATETIME
DECLARE @volume FLOAT
DECLARE @RepID INT
DECLARE @Month INT
DECLARE @Year INT

SET @repID = 80
SET @Month = 1
SET @year = 2012

/* the actual cursor */
DECLARE commission_cursor CURSOR FOR
  SELECT AccountNo,
         supplier,
         commodity,
         meterStart,
         MeterEnd,
         Volume
  FROM   commission
  WHERE  Datepart(m, PaymentDate) = @Month
         AND Datepart(YYYY, PaymentDate) = @Year

OPEN commission_cursor

FETCH next FROM commission_cursor INTO @AccountNo, @supplier, @commodity, @MeterStart, @MeterEnd, @Volume;

WHILE @@fetch_status = 0
  BEGIN
      IF EXISTS (SELECT id
                 FROM   Records
                 WHERE  AccountNo = @AccountNo
                        AND supplier = @supplier
                        AND Commodity = @commodity
                        AND RepID = @repID)
        INSERT INTO #result
        SELECT TOP 1 RepID,
                     AccountNo,
                     Supplier,
                     CompanyName,
                     [Supplier Start Date],
                     [Supplier End Date],
                     Product,
                     Commodity,
                     [customer end date],
                     [Expected Usage],
                     EnrollStatus,
                     ActualStartDate,
                     ActualEndDate,
                     @meterstart,
                     @MeterEnd,
                     @volume
        FROM   Records
        WHERE  AccountNo = @AccountNo
               AND supplier = @supplier
               AND Commodity = @commodity
               AND RepID = @repID
               AND @MeterStart >= Dateadd(dd, -7, ActualStartDate)
               AND @meterEnd <= Isnull(Dateadd(dd, 30, ActualEndDate), '2015-12-31')

      FETCH next FROM commission_cursor INTO @AccountNo, @supplier, @commodity, @MeterStart, @MeterEnd, @Volume;
  END

SELECT *
FROM   #result

/* clean up */
CLOSE commission_cursor

DEALLOCATE commission_cursor

DROP TABLE #result 

Я прочитал ответ на Как ускорить курсор T-SQL?, для этого я переписываю этот запрос в виде таблицы. Но у меня есть еще один запрос, который использует соединение и быстро выполняется. Проблема в том, что он не может различать дубликаты в моей таблице records.

Есть ли что-нибудь, что я могу сделать, чтобы сделать это быстрее. Это первичный вопрос. Если нет, есть ли у вас альтернативный способ сделать это.

Мне конкретно нужна помощь с

  • Поможет ли использование представлений или процедур хранения
  • У меня есть способ использовать кеш в курсоре, чтобы сделать его быстрее
  • Любая другая опция в синтаксисе

person Hammad Khan    schedule 15.06.2012    source источник
comment
Вы заметили, что курсор в связанном вопросе был объявлен с опцией FAST_FORWARD?   -  person Andriy M    schedule 15.06.2012
comment
Я сделал то, что я не знаю, что это значит. Потому что он использовал курсор в курсоре, который, вероятно, здесь не применяется.   -  person Hammad Khan    schedule 15.06.2012
comment
Основная проблема с производительностью курсора возникает при использовании параметров по умолчанию (глобальный, динамический, чтение/запись, прокручиваемый). Прочтите эту статью: sqlblog.com/blogs/aaron_bertrand/archive/2012/01/26/ Это не означает, что добавление LOCAL STATIC READ_ONLY FORWARD_ONLYединственный способ повысить эффективность. (вполне вероятно, что вы можете сделать это вообще без цикла), но это хорошее начало.   -  person Aaron Bertrand    schedule 15.06.2012
comment
Я бы полностью избавился от курсора и написал запрос, который дает вам одну запись, которую вы хотите для вашей комиссии, а затем использовал ее в качестве подзапроса или сбрасывал ее во временную таблицу. вы должны иметь возможность получить столько записей за секунды, а не за минуты   -  person Limey    schedule 15.06.2012
comment
Также вы можете объяснить TOP 1 без ORDER BY? Вас действительно не волнует, какая совпадающая строка в Records вы получите?   -  person Aaron Bertrand    schedule 15.06.2012
comment
@AaronBertrand, я не использовал order by, чтобы еще больше не перегружать курсор. В идеале да, я хочу его там. Но я уже фильтрую записи на основе того, попадают ли даты начала и окончания счетчика в эту запись, а затем выбираю только их. Если таких записей более 1, то можно использовать любую из них.   -  person Hammad Khan    schedule 15.06.2012
comment
Итак, каким должен быть результирующий порядок? Сейчас это не определено. Даже ваш select * from result, кажется, не заботится о порядке.   -  person Aaron Bertrand    schedule 15.06.2012
comment
Какие индексы есть в двух таблицах?   -  person Martin Smith    schedule 15.06.2012
comment
@MartinSmith, не знаю об индексах. У них есть только 1 первичный ключ. Аарон, в моей таблице результатов есть только результат, поэтому мне не нужно применять там порядок или фильтровать. Я проверю ваш обновленный запрос.   -  person Hammad Khan    schedule 15.06.2012
comment
почему -2 голоса в течение 10 минут каждого после 8 дней с момента постановки вопроса?   -  person Hammad Khan    schedule 25.06.2012


Ответы (2)


Самый первый вариант — установить наименее ресурсоемкие параметры для вашего курсора:

declare commission_cursor cursor
local static read_only forward_only
for 

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

;WITH x AS 
(
  SELECT 
    rn = ROW_NUMBER() OVER (PARTITION BY r.AccountNo, r.Supplier, r.Commodity, r.RepID 
      ORDER BY r.ActualEndDate DESC),
    r.RepID, 
    r.AccountNo, 
    r.Supplier, 
    r.CompanyName, 
    StartDate = r.[Supplier Start Date], 
    EndDate = r.[Supplier End Date],
    r.Product, 
    r.Commodity, 
    ContractEnd = r.[customer end date], 
    EstUsage = r.[Expected Usage], 
    r.EnrollStatus, 
    EnrollDate = r.ActualStartDate,
    r.ActualEndDate, 
    c.MeterStart, 
    c.MeterEnd, 
    ActualUsage = c.Volume 
  FROM dbo.commission AS c 
  INNER JOIN dbo.Records AS r
    ON c.AccountNo = r.AccountNo
    AND c.Supplier = r.Supplier
    AND c.Commodity = r.Commodity
    AND c.RepID = r.RepID
  WHERE 
    c.PaymentDate >= DATEADD(MONTH, @Month-1, CONVERT(CHAR(4), @Year) + '0101')
    AND c.PaymentDate < DATEADD(MONTH, 1, CONVERT(CHAR(4), @Year) + '0101')
    AND r.RepID = @RepID
)
SELECT RepID, AccountNo, Supplier, CompanyName, StartDate, EndDate, 
  Product, Commodity, ContractEnd, EstUsage, EnrollStatus, EnrollDate, 
  ActualEndDate, MeterStart, MeterEnd, ActualUsage 
FROM x 
WHERE rn = 1 --ORDER BY something;

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

person Aaron Bertrand    schedule 15.06.2012
comment
+1 Курсоры - это дьявол, очень высока вероятность того, что это может быть с помощью обычного оператора sql. - person Erik Philips; 15.06.2012
comment
Этот запрос выполняется быстро, почти мгновенно, но возвращает 9000 записей вместо 400. Над этим нужно поработать. - person Hammad Khan; 15.06.2012
comment
Извините, я просто пропустил переменную @RepID в предложении where внутри курсора (я думаю, что она была потеряна при форматировании до того, как Мартин ее исправил). Пожалуйста, попробуйте мой обновленный код. Ваша первая попытка запроса никогда не должна быть курсором, хотя я немного менее уверен в том, что их никогда не следует использовать - у них есть свое место (например, промежуточные итоги). - person Aaron Bertrand; 16.06.2012

Временные таблицы — ваш друг

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

Создайте таблицу #temp, извлеките объединенные данные из обеих таблиц. Убедитесь, что вы включили поля ID в обе таблицы, даже если вам это не требуется. Это поможет удалить дубликаты.

Теперь вы можете выполнять любые вычисления в этой таблице. Удалите дубликаты из таблицы B, просто удалите повторяющиеся идентификаторы таблицы B. Удалите дубликаты из таблицы A, просто удалите повторяющиеся идентификаторы таблицы A. Проблема более сложная, но, по крайней мере, это, вероятно, лучший способ решить вашу проблему и значительно ускорить ее, если курсоры слишком дороги и требуют много времени для расчета. В моем случае это заняло +5 мин. Запрос таблицы #temp занимает около 5 секунд, в нем гораздо больше вычислений.


При применении решения Аарона курсор не стал быстрее. Второй запрос был быстрее, но он не дал мне правильного ответа, поэтому в конце концов я использовал временные таблицы. Это мой собственный ответ.

person Hammad Khan    schedule 26.06.2012