Как преобразовать строку по выполнению строки в подход на основе SET в SQL - PullRequest
1 голос
/ 16 июня 2019

Я работаю над огромным кодом SQL, и, к сожалению, у него есть CURSOR, который обрабатывает еще два вложенных CURSORS внутри (всего три курсора внутри хранимой процедуры), который обрабатывает миллионы данных, которые будут DELETE, UPDATE и INSERT.Это занимает много времени из-за построчного выполнения, и я хочу изменить это на подход, основанный на SET

Во многих статьях показано, что использование CURSOR не рекомендуется, и альтернативой является использование циклов WHILE вместоИтак, я попробовал и заменил три CUROSR на три цикла WHILE, но получаю тот же результат, но улучшения в производительности нет, это заняло столько же времени, сколько и для CUROSR.

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

declare @projects table (
    ProjectID INT,
    fieldA int,
    fieldB int,
    fieldC int,
    fieldD int)

    INSERT INTO @projects
    SELECT ProjectID,fieldA,fieldB,fieldC, fieldD
    FROM ProjectTable

    DECLARE projects1 CURSOR LOCAL FOR /*First cursor - fetch the cursor from ProjectaTable*/
        Select ProjectID FROM @projects

    OPEN projects1
    FETCH NEXT FROM projects1 INTO @ProjectID
    WHILE @@FETCH_STATUS = 0
    BEGIN
    BEGIN TRY
        BEGIN TRAN

        DELETE FROM T_PROJECTGROUPSDATA td
        WHERE td.ID = @ProjectID

        DECLARE datasets CURSOR FOR /*Second cursor - this will get the 'collectionDate'field from datasetsTable for every project fetched in above cursor*/
            Select DataID, GroupID, CollectionDate
            FROM datasetsTable 
            WHERE datasetsTable.projectID = @ProjectID /*lets say this will fetch ten records for a single projectID*/

        OPEN datasets
        FETCH NEXT FROM datasets INTO @DataID, @GroupID, @CollectionDate
        WHILE @@FETCH_STATUS = 0
        BEGIN

            DECLARE period CURSOR FOR /*Third Cursor - this will process the records from another table called period with above fetched @collectionDate*/
            SELECT ID, dbo.fn_GetEndOfPeriod(ID) 
            FROM T_PERIODS
            WHERE DATEDIFF(dd,@CollectionDate,dbo.fn_GetEndOfPeriod(ID)) >= 0 /*lets say this will fetch 20 records for above fetched single @CollectionDate*/
            ORDER BY [YEAR],[Quarter]

                OPEN period
                FETCH NEXT FROM period INTO @PeriodID, @EndDate
                WHILE @@FETCH_STATUS = 0
                BEGIN
                    IF EXISTS (some conditions No - 1 )
                    BEGIN
                        BREAK
                    END
                    IF EXISTS (some conditions No - 2 )
                    BEGIN
                        FETCH NEXT FROM period INTO @PeriodID, @EndDate
                        CONTINUE
                    END

                    /*get the appropirate ID from T_uploads table for the current projectID and periodID fetched*/
                    SET @UploadID = (SELECT ID FROM T_UPLOADS u WHERE  u.project_savix_ID = @ProjectID AND u.PERIOD_ID = @PeriodID AND u.STATUS = 3)

                    /*Update some fields in T_uploads table for the current projectID and periodID fetched*/
                    UPDATE T_uploads
                    SET fieldA = mp.fieldA, fieldB = mp.fieldB
                    FROM @projects mp
                    WHERE T_UPLOADS.ID = @UploadID AND mp.ProjectID = @ProjectID

                    /*Insert some records in T_PROJECTGROUPSDATA table for the current projectID and periodID fetched*/
                    INSERT INTO T_PROJECTGROUPSDATA tpd ( fieldA,fieldB,fieldC,fieldD,uploadID)
                    SELECT fieldA,fieldB,fieldC,fieldD,@UploadID
                    FROM @projects
                    WHERE tpd.DataID = @DataID

                FETCH NEXT FROM period INTO @PeriodID, @EndDate
                END
                CLOSE period
                DEALLOCATE period

            FETCH NEXT FROM datasets INTO @DataID, @GroupID, @CollectionDate, @Status, @Createdate
        END

        CLOSE datasets
        DEALLOCATE datasets

        COMMIT
    END TRY

    BEGIN CATCH
        Error handling
        IF @@TRANCOUNT > 0
            ROLLBACK
    END CATCH
    FETCH NEXT FROM projects1 INTO @ProjectID, @FAID
END


CLOSE projects1
DEALLOCATE projects1

SELECT 1 as success

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

Ответы [ 3 ]

2 голосов
/ 16 июня 2019

До тех пор, пока структура таблицы и данные об ожидаемом результате не предоставлены, я вижу несколько быстрых вещей, которые я могу улучшить (некоторые из них уже упомянуты другими выше):

  1. WHILEЦикл также является курсором.Таким образом, переключение на цикл while не приведет к ускорению.
  2. Используйте курсор LOCAL FAST_FORWARD, если только вам не нужно отслеживать запись.Это сделало бы выполнение намного быстрее.
  3. Да, я согласен, что подход на основе SET был бы самым быстрым в большинстве случаев, однако, если вы должны где-то хранить промежуточный набор результатов, я бы предложил использовать временныйтаблица вместо табличной переменной.Временная таблица - это «меньшее зло» между этими двумя вариантами.Вот несколько причин, по которым вам следует избегать использования табличной переменной:

    • Поскольку SQL Server не будет иметь каких-либо предварительных статистических данных о табличной переменной во время построения плана выполнения, он всегда будет учитывать, что толькоодна запись будет возвращена табличной переменной во время построения плана выполнения.И соответственно Storage Engine будет выделять только столько оперативной памяти для выполнения запроса.Но в действительности могут быть миллионы записей, которые табличная переменная может хранить во время выполнения.Если это произойдет, SQL Server будет вынужден пролить данные на жесткий диск во время выполнения (и вы увидите много PAGEIOLATCH в sys.dm_os_wait_stats), что замедляет выполнение запросов.
    • Один из способов избавиться от вышеупомянутогопроблема заключалась бы в предоставлении подсказки уровня оператора OPTION (RECOMPILE) в конце каждого запроса, где используется табличное значение.Это заставит SQL Server составлять план выполнения этих запросов каждый раз во время выполнения, и можно избежать проблемы с меньшим выделением памяти.Однако недостатком этого является то, что SQL Server больше не сможет использовать уже кэшированный план выполнения для этой хранимой процедуры и будет каждый раз требовать перекомпиляции, что в некоторой степени ухудшит производительность.Таким образом, если вы не знаете, что данные в базовой таблице часто изменяются или сама хранимая процедура выполняется не часто, Microsoft MVP не рекомендует этот подход.
1 голос
/ 17 июня 2019

Я думаю, что весь код курсоров, приведенный выше, можно упростить до чего-то подобного:

DROP TABLE IF EXISTS #Source;
SELECT DISTINCT p.ProjectID,p.fieldA,p.fieldB,p.fieldC,p.fieldD,u.ID AS [UploadID]
INTO #Source
FROM ProjectTable p
INNER JOIN DatasetsTable d ON d.ProjectID = p.ProjectID
INNER JOIN T_PERIODS s ON DATEDIFF(DAY,d.CollectionDate,dbo.fn_GetEndOfPeriod(s.ID)) >= 0 
INNER JOIN T_UPLOADS u ON u.roject_savix_ID = p.ProjectID AND u.PERIOD_ID = s.ID AND u.STATUS = 3
WHERE NOT EXISTS (some conditions No - 1)
    AND NOT EXISTS (some conditions No - 2)
;

UPDATE u SET u.fieldA = s.fieldA, u.fieldB = s.fieldB
FROM T_UPLOADS u
INNER JOIN #Source s ON s.UploadID = u.ID
;
INSERT INTO T_PROJECTGROUPSDATA (fieldA,fieldB,fieldC,fieldD,uploadID)
SELECT DISTINCT s.fieldA,s.fieldB,s.fieldC,s.fieldD,s.UploadID
FROM #Source s
;

DROP TABLE IF EXISTS #Source;

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

1 голос
/ 16 июня 2019

Замена Cursor вслепую While не является рекомендуемым вариантом, поэтому он не повлияет на производительность и даже может негативно повлиять на производительность.

Когда вы определяете курсор с помощью Declare C Cursor на самом деле вы собираетесь создать курсор SCROLL, который указывает, что доступны все параметры выборки (FIRST, LAST, PRIOR, NEXT, RELATIVE, ABSOLUTE).

Когда вам нужно просто Fetch Next в качестве параметра прокрутки, вы можете объявить курсор какFAST_FORWARD

Вот цитата о FAST_FORWARD курсоре в Microsoft документы:

Указывает, что курсор может двигаться только вперед и прокручиваться изс первого по последний ряд.FETCH NEXT - единственная поддерживаемая опция извлечения.Все операторы вставки, обновления и удаления, сделанные текущим пользователем (или зафиксированные другими пользователями), которые влияют на строки в результирующем наборе, видны при выборке строк.Однако, поскольку курсор нельзя прокручивать назад, изменения, внесенные в строки в базе данных после извлечения строки, не видны через курсор.Прямые курсоры по умолчанию являются динамическими, что означает, что все изменения обнаруживаются при обработке текущей строки.Это обеспечивает более быстрое открытие курсора и позволяет результирующему набору отображать обновления, внесенные в базовые таблицы.Хотя курсоры только вперед не поддерживают обратную прокрутку, приложения могут вернуться к началу набора результатов, закрыв и снова открыв курсор.

Таким образом, вы можете объявить свои курсоры, используя DECLARE <CURSOR NAME> FAST_FORWARD FOR ..., и выполучить заметные улучшения

...