Как лучше всего выполнять манипуляции с огромными объемами данных в SQL Server? - PullRequest
3 голосов
/ 05 марта 2010

Нам нужно выполнить следующую операцию в нашей базе данных:

Существует таблица A, в которой столбец B_ID является внешним ключом таблицы B. В таблице A имеется много строк с одинаковым значением B_ID, и мы хотим исправить это путем клонирования соответствующих строк в B и перенаправить строки от А до них.

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

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

Помимо какой-либо очистки журнала, есть ли способ обработки массовых вставок / обновлений данных в SQL Server, который был бы быстрее и вообще не взрывал журнал?

Ответы [ 6 ]

2 голосов
/ 05 марта 2010

Вот еще один способ сделать это в пакете (без курсоров).@ KM выглядит так, как будто оно должно работать, но для меня это выглядит немного медленно / страшно с большим количеством операций блокировки и сканирования;если вы ограничиваете рабочий набор только новыми строками, то он должен быть довольно быстрым.

Вот сценарий установки для тестовых данных:

CREATE TABLE Colors
(
    ColorID int NOT NULL IDENTITY(1, 1) PRIMARY KEY,
    ColorName varchar(50) NOT NULL
)

CREATE TABLE Markers
(
    MarkerID int NOT NULL IDENTITY(1, 1) PRIMARY KEY,
    MarkerName varchar(50) NOT NULL,
    ColorID int NOT NULL,
    CONSTRAINT FK_Markers_Colors FOREIGN KEY (ColorID)
        REFERENCES Colors (ColorID)
)

INSERT Colors (ColorName) VALUES ('Red')
INSERT Colors (ColorName) VALUES ('Green')
INSERT Colors (ColorName) VALUES ('Blue')

INSERT Markers (MarkerName, ColorID) VALUES ('Test1', 1)
INSERT Markers (MarkerName, ColorID) VALUES ('Test2', 1)
INSERT Markers (MarkerName, ColorID) VALUES ('Test3', 1)
INSERT Markers (MarkerName, ColorID) VALUES ('Test4', 2)
INSERT Markers (MarkerName, ColorID) VALUES ('Test5', 2)
INSERT Markers (MarkerName, ColorID) VALUES ('Test6', 3)
INSERT Markers (MarkerName, ColorID) VALUES ('Test7', 3)

Итак, у нас есть 1: Many имы хотим сделать это 1: 1.Чтобы сделать это, сначала поставьте в очередь список обновлений (мы индексируем это по некоторому другому множеству уникальных столбцов, чтобы ускорить объединение позже):

CREATE TABLE #NewColors
(
    MarkerID int NOT NULL,
    ColorName varchar(50) NOT NULL,
    Seq int NOT NULL,
    CONSTRAINT PK_#NewColors PRIMARY KEY (MarkerID)
)

CREATE INDEX IX_#NewColors
ON #NewColors (ColorName, Seq);

WITH Refs AS
(
    SELECT
        MarkerID,
        ColorID,
    ROW_NUMBER() OVER (PARTITION BY ColorID ORDER BY (SELECT 1)) AS Seq
    FROM Markers
)
INSERT #NewColors (MarkerID, ColorName, Seq)
SELECT r.MarkerID, c.ColorName, r.Seq - 1
FROM Refs r
INNER JOIN Colors c
    ON c.ColorID = r.ColorID
WHERE r.Seq > 1

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

DECLARE @InsertedColors TABLE
(
    ColorID int NOT NULL PRIMARY KEY,
    ColorName varchar(50) NOT NULL
)

INSERT Colors (ColorName)
OUTPUT inserted.ColorID, inserted.ColorName
INTO @InsertedColors
    SELECT ColorName
    FROM #NewColors nc;

И, наконец, объедините его (вот где этот дополнительный индекс для временной таблицы пригодится):

WITH InsertedColorSeq AS
(
    SELECT
        ColorID, ColorName,
        ROW_NUMBER() OVER (PARTITION BY ColorName ORDER BY ColorID) AS Seq
    FROM @InsertedColors
),
Updates AS
(
    SELECT nc.MarkerID, ic.ColorID AS NewColorID
    FROM #NewColors nc
    INNER JOIN InsertedColorSeq ic
    ON ic.ColorName = nc.ColorName
    AND ic.Seq = nc.Seq
)
MERGE Markers m
USING Updates u
    ON m.MarkerID = u.MarkerID
WHEN MATCHED THEN
    UPDATE SET m.ColorID = u.NewColorID;

DROP TABLE #NewColors

This должен быть очень эффективным, потому что он когда-либо запрашивает производственные таблицы только один раз.Все остальное будет работать с относительно небольшими данными в временных таблицах.

Проверьте результаты:

SELECT m.MarkerID, m.MarkerName, c.ColorID, c.ColorName
FROM Markers m
INNER JOIN Colors c
    ON c.ColorID = m.ColorID

Вот наш вывод:

MarkerID     MarkerName   ColorID   ColorName
1            Test1        1         Red
2            Test2        6         Red
3            Test3        7         Red
4            Test4        2         Green
5            Test5        5         Green
6            Test6        3         Blue
7            Test7        4         Blue

Это должно бытьчто хочешь, верно?Нет курсоров, нет серьезного безобразия.Если он отнимает слишком много памяти или места в базе данных tempdb, вы можете заменить переменную временная таблица / таблица индексированной физической промежуточной таблицей.Даже с несколькими миллионами строк это никоим образом не должно заполнить журнал транзакций и привести к сбою.

2 голосов
/ 05 марта 2010

Я не уверен, как это будет работать на множестве строк, но попробуйте:

DECLARE @TableA table (RowID int, B_ID int)
INSERT INTO @TableA VALUES (1,1)
INSERT INTO @TableA VALUES (2,1) --need to copy
INSERT INTO @TableA VALUES (3,2)
INSERT INTO @TableA VALUES (4,2) --need to copy
INSERT INTO @TableA VALUES (5,2) --need to copy
INSERT INTO @TableA VALUES (6,1) --need to copy
INSERT INTO @TableA VALUES (7,3)
INSERT INTO @TableA VALUES (8,3) --need to copy
DECLARE @TableB table (B_ID int, BValues varchar(10))
INSERT INTO @TableB VALUES (1,'one')
INSERT INTO @TableB VALUES (2,'two')
INSERT INTO @TableB VALUES (3,'three')

DECLARE @Max_B_ID int
SELECT @Max_B_ID=MAX(B_ID) FROM @TableB

--if you are using IDENTITY, turn them off here
INSERT INTO @TableB 
        (B_ID, BValues)
        --possibly capture the data to eliminate duplication??
        --OUTPUT INSERTED.tableID, INSERTED.datavalue
        --INTO @y 
    SELECT
        dt.NewRowID, dt.BValues
        FROM (SELECT 
                  RowID, a.B_ID
                      ,@Max_B_ID+ROW_NUMBER() OVER(order by a.B_ID) AS NewRowID,b.BValues
                  FROM (SELECT
                            RowID, B_ID
                            FROM (SELECT 
                                      RowID, a.B_ID, ROW_NUMBER() OVER(PARTITION by a.B_ID order by a.B_ID) AS RowNumber
                                      FROM @TableA a
                                 ) dt
                            WHERE dt.RowNumber>1
                       )a
                      INNER JOIN @TableB  b ON a.B_ID=b.B_ID
             ) dt


UPDATE aa
    SET B_ID=NewRowID
    FROM @TableA   aa
        INNER JOIN (SELECT
                        dt.NewRowID, dt.BValues,dt.RowID
                        FROM (SELECT 
                                  RowID, a.B_ID
                                      ,@Max_B_ID+ROW_NUMBER() OVER(order by a.B_ID) AS NewRowID,b.BValues
                                  FROM (SELECT
                                            RowID, B_ID
                                            FROM (SELECT 
                                                      RowID, a.B_ID, ROW_NUMBER() OVER(PARTITION by a.B_ID order by a.B_ID) AS RowNumber
                                                      FROM @TableA a
                                                 ) dt
                                            WHERE dt.RowNumber>1
                                       )a
                                      INNER JOIN @TableB  b ON a.B_ID=b.B_ID
                             ) dt
                   ) dt2 ON aa.RowID=dt2.RowID

SELECT * FROM @TableA
SELECT * FROM @TableB

ВЫХОД:

RowID       B_ID
----------- -------
1           1
2           4
3           2
4           6
5           7
6           5
7           3
8           8

(8 row(s) affected)

B_ID        BValues
----------- -------
1           one
2           two
3           three
4           one
5           one
6           two
7           two
8           three

(8 row(s) affected)
2 голосов
/ 05 марта 2010

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

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

Вы можете попробовать выполнить работу в SSIS, но я не знаю, поможет ли это на самом деле проблема с журналированием.Это поможет улучшить производительность при выполнении задачи.

2 голосов
/ 05 марта 2010

Если вы можете перевести операцию в автономный режим, вы можете изменить модель восстановления базы данных, внести изменения, а затем изменить модель восстановления обратно.

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

ПРИМЕЧАНИЕ: при таком подходе сначала обязательно сделайте чертовски хорошую резервную копию ...

0 голосов
/ 05 марта 2010

Вот что я делаю:

Создайте запрос, который возвращает данные из двух таблиц (A, B) в точности так, как они должен быть в финальной таблице (C) и поместить это в файл ExtractData.sql:

select
    A.id,
    A.xxx,
    A.yyy,
    B.*
from
   A

   JOIN B
     on B.id = A.id

Затем в окне cmd выполните эту команду, чтобы извлечь данные в файл:

sqlcmd.exe -S [Server] -U [user] -P [pass] -d [dbname] -i DataExtract.sql -s "|" -h -1 -W -o ExtractData.dat

Чтобы избежать заполнения логов, попробуйте установить простой режим восстановления БД перед вставкой:

ALTER DATABASE [database name] SET RECOVERY SIMPLE

Затем выполните TRUNCATE TABLE C (если вам нужно очистить старые данные - они не добавляются в журналы, как удаляются).

Затем в окне cmd выполните эту команду для массовой загрузки данных в таблицу C:

bcp.exe dbname.dbo.C in ExtractData.dat -S [Server] -U [user] -P [pass] -t "|" -e ExtractData.err -r \n -c

Записи об ошибках будут отображаться в файле ExtractData.err, поэтому, если вам нужно настроить Схема таблицы C вы можете настроить / усечь / повторно загрузить извлеченные данные, чтобы вы не нужно каждый раз запускать запрос.

затем установите режим восстановления обратно на FULL после того, как вы закончите:

ALTER DATABASE [database name] SET RECOVERY FULL
0 голосов
/ 05 марта 2010

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

Таким образом, вы полностью избавляетесь от B и можете внести изменения в одном запросе на обновление. Что-то вроде:

update tableA SET
  col1 = B.col1,
  col2 = B.col2
from tableA A
inner join tableB on (B.ID = A.B_ID)
...