Пакетное удаление SQL - PullRequest
11 голосов
/ 22 мая 2009

У меня есть таблица в SQL Server 2005, в которой около 4 миллиардов строк. Мне нужно удалить примерно 2 миллиарда этих строк. Если я пытаюсь сделать это в одной транзакции, журнал транзакций заполняется, и он терпит неудачу. У меня нет свободного места, чтобы увеличить журнал транзакций. Я полагаю, что лучший способ продвинуться - это объединить операторы удаления (партиями ~ 10 000?).

Я, вероятно, могу сделать это с помощью курсора, но это стандартный / простой / умный способ сделать это?

P.S. Эта таблица не имеет столбца идентификаторов в качестве PK. PK состоит из целочисленного внешнего ключа и даты.

Ответы [ 9 ]

8 голосов
/ 22 мая 2009

Что отличает строки, которые вы хотите удалить от тех, которые вы хотите сохранить? Будет ли это работать для вас:

while exists (select 1 from your_table where <your_condition>)
delete top(10000) from your_table
where <your_condition>
7 голосов
/ 22 мая 2009

Вы можете «откусить» удаление, что также означает, что вы не вызываете большую нагрузку на базу данных. Если ваши резервные копии t-log запускаются каждые 10 минут, то вы можете запустить это один или два раза за один и тот же интервал. Вы можете запланировать его как задание агента SQL

попробуйте что-то вроде этого:

DECLARE @count int
SET @count = 10000

    DELETE  FROM table1 
    WHERE table1id IN (
        SELECT TOP (@count) tableid
        FROM table1
        WHERE x='y'
    )
3 голосов
/ 22 мая 2009

Похоже, это одноразовая операция (я надеюсь на вас), и вам не нужно возвращаться в состояние, которое находится на полпути этого пакетного удаления - если это так, почему бы вам просто не переключиться в режим транзакций ПРОСТОЙ перед запуском, а затем вернуться к полному, когда вы закончите?

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

вы можете сделать это в вашем скрипте с помощью smt:

ALTER DATABASE myDB SET RECOVERY FULL/SIMPLE

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

2 голосов
/ 22 мая 2009

Помимо помещения этого пакета с инструкцией по усечению журнала, вы также можете попробовать следующие приемы:

  • Добавьте критерии, соответствующие первому столбцу в вашем кластеризованном индексе, в дополнение к другим критериям
  • Удалите все индексы из таблицы и затем верните их после того, как удаление будет выполнено, если это возможно, и не будет мешать чему-либо еще происходящему в БД, но СОХРАНИТЕ кластерный индекс

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

DECLARE @max_id INT, @start_id INT, @end_id INT, @interval INT
SELECT @start_id = MIN(id), @max_id = MAX(id) FROM My_Table
SET @interval = 100000  -- You need to determine the right number here
SET @end_id = @start_id + @interval

WHILE (@start_id <= @max_id)
BEGIN
     DELETE FROM My_Table WHERE id BETWEEN @start_id AND @end_id AND <your criteria>

     SET @start_id = @end_id + 1
     SET @end_id = @end_id + @interval
END
2 голосов
/ 22 мая 2009

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

Только мои два человека.

2 голосов
/ 22 мая 2009

Ну, если бы вы использовали SQL Server Partitioning, скажем, на основе столбца даты, вы бы, возможно, отключили разделы, которые больше не нужны. Подумайте о возможной реализации в будущем.

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

Вы также можете рассмотреть следующий метод:

  1. Скопируйте данные, чтобы сохранить во временную таблицу
  2. Усечение исходной таблицы для очистки всех данных
  3. Переместить все из временной таблицы обратно в исходную таблицу

Ваши индексы также будут перестроены при добавлении данных обратно в исходную таблицу.

0 голосов
/ 04 сентября 2014

Вот мой пример:

-- configure script
-- Script limits - transaction per commit (default 10,000)
-- And time to allow script to run (in seconds, default 2 hours)
--
DECLARE @MAX INT
DECLARE @MAXT INT
--
-- These 4 variables are substituted by shell script.
--
SET @MAX = $MAX
SET @MAXT = $MAXT
SET @TABLE = $TABLE
SET @WHERE = $WHERE

-- step 1 - Main loop
DECLARE @continue INT
-- deleted in one transaction
DECLARE @deleted INT
-- deleted total in script
DECLARE @total INT
SET @total = 0
DECLARE @max_id INT, @start_id INT, @end_id INT, @interval INT
SET @interval = @MAX
SELECT @start_id = MIN(id), @max_id = MAX(id) from @TABLE
SET @end_id = @start_id + @interval

-- timing
DECLARE @start DATETIME
DECLARE @now DATETIME
DECLARE @timee INT
SET @start = GETDATE()
-- 
SET @continue = 1
IF OBJECT_ID (N'EntryID', 'U') IS NULL 
BEGIN
    CREATE TABLE EntryID (startid INT)
    INSERT INTO EntryID(startid) VALUES(@start_id)
END
    ELSE
BEGIN
    SELECT @start_id = startid FROM EntryID
END


WHILE (@continue = 1 AND @start_id <= @max_id)
BEGIN

    PRINT 'Start issued:   ' + CONVERT(varchar(19), GETDATE(), 120)
    BEGIN TRANSACTION
        DELETE 
        FROM @TABLE
        WHERE id BETWEEN @start_id AND @end_id AND @WHERE
        SET @deleted = @@ROWCOUNT
    UPDATE EntryID SET EntryID.startid = @end_id + 1
    COMMIT
    PRINT 'Deleted issued: ' + STR(@deleted) + ' records. ' + CONVERT(varchar(19), GETDATE(), 120) 
    SET @total = @total + @deleted
    SET @start_id = @end_id + 1
    SET @end_id = @end_id + @interval
    IF @end_id > @max_id
        SET @end_id = @max_id

    SET @now = GETDATE()
    SET @timee = DATEDIFF (second, @start, @now)
    if @timee > @MAXT
    BEGIN
    PRINT 'Time limit exceeded for the script, exiting'
    SET @continue = 0
    END
--    ELSE
--    BEGIN
--      SELECT @total 'Removed now', @timee 'Total time, seconds'   
--    END
END

SELECT @total 'Removed records', @timee 'Total time sec' , @start_id 'Next id', @max_id 'Max id', @continue 'COMPLETED? '
SELECT * from EntryID next_start_id

GO
0 голосов
/ 22 мая 2009

Я согласен с людьми, которые хотят, чтобы вы просматривали меньший набор записей, это будет быстрее, чем пытаться выполнить всю операцию за один шаг. Вы можете испытать количество записей, которые вы должны включить в цикл. Около 2000 в то время, кажется, самое приятное место в большинстве таблиц, где я делаю большие разряды, хотя некоторым нужно меньше, например, 500. Зависит от количества внешних ключей, размера записи, триггеров и т. Д., Так что это действительно займет некоторые эксперименты, чтобы найти то, что вам нужно. Это также зависит от того, насколько интенсивно использование стола. Таблица с большим доступом будет нуждаться в каждой итерации цикла для более короткого времени. Если вы можете работать в нерабочее время или лучше всего в однопользовательском режиме, то вы можете удалить больше записей за один цикл.

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

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

Обычно рекомендуется создать резервную копию базы данных непосредственно перед выполнением операции такого рода.

0 голосов
/ 22 мая 2009

Короткий ответ: вы не можете удалить 2 миллиарда строк без каких-либо серьезных простоев базы данных.

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

Вам нужно будет удалить как можно больше строк, пока журнал транзакций не заполнится, а затем обрезать его каждый раз. Ответ, предоставленный Станиславом Князевым, можно изменить, чтобы сделать это, увеличив размер пакета и добавив вызов для усечения файла журнала.

...