УДАЛИТЬ SQL с коррелированным подзапросом для таблицы с 42 миллионами строк?

У меня есть таблица cats с 42 795 120 строками.

Судя по всему, это много строк. Итак, когда я это сделаю:

/* owner_cats is a many-to-many join table */
DELETE FROM cats
WHERE cats.id_cat IN (
SELECT owner_cats.id_cat FROM owner_cats
WHERE owner_cats.id_owner = 1)

время запроса истекло :(

(изменить: мне нужно увеличить CommandTimeout значение, по умолчанию всего 30 секунд)

Я не могу использовать TRUNCATE TABLE cats, потому что не хочу отрывать кошек от других владельцев.

Я использую SQL Server 2005 с параметром «Модель восстановления», установленным на «Простое».

Итак, я подумал о том, чтобы сделать что-то вроде этого (выполнить этот SQL из приложения, кстати):

DELETE TOP (25) PERCENT FROM cats
WHERE cats.id_cat IN (
SELECT owner_cats.id_cat FROM owner_cats
WHERE owner_cats.id_owner = 1)

DELETE TOP(50) PERCENT FROM cats
WHERE cats.id_cat IN (
SELECT owner_cats.id_cat FROM owner_cats
WHERE owner_cats.id_owner = 1)

DELETE FROM cats
WHERE cats.id_cat IN (
SELECT owner_cats.id_cat FROM owner_cats
WHERE owner_cats.id_owner = 1)

У меня вопрос: каков порог количества строк, которое я могу DELETE в SQL Server 2005?

Или, если мой подход не оптимален, предложите лучший подход. Спасибо.

Этот пост мне мало помог:

РЕДАКТИРОВАТЬ (6 августа 2010 г.):

Хорошо, я только что понял, снова прочитав приведенную выше ссылку, что у меня нет индексов для этих таблиц. Кроме того, некоторые из вас уже указали на эту проблему в комментариях ниже. Имейте в виду, что это фиктивная схема, поэтому даже id_cat не является PK, потому что в моей реальной схеме это не уникальное поле.

Я поставлю индексы на:

  1. cats.id_cat
  2. owner_cats.id_cat
  3. owner_cats.id_owner

Думаю, я все еще осваиваю это хранилище данных, и, очевидно, мне нужны индексы по всем JOIN полям, верно?

Однако на выполнение этой пакетной загрузки у меня уходит несколько часов. Я уже делаю это как SqlBulkCopy (кусками, а не сразу по 42 миллиона). У меня есть индексы и ПК. Я прочитал следующие сообщения, которые подтверждают мою теорию о том, что индексы замедляются даже при массовом копировании:

Итак, я собираюсь DROP свои индексы перед копированием, а затем CREATE заново их, когда это будет сделано.

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

ОБНОВЛЕНИЕ (07.08.2010):

Том предложил:

DELETE
FROM cats c
WHERE EXISTS (SELECT 1
FROM owner_cats o
WHERE o.id_cat = c.id_cat
AND o.id_owner = 1)

И все же без индексов для 42 миллионов строк потребовалось 13:21 мин: сек по сравнению с 22:08, как описано выше. Однако на 13 миллионов строк у него ушло 2:13 против 2:10 по моему старому пути. Это отличная идея, но мне все равно нужно использовать индексы!

Обновление (8.08.2010):

Что-то ужасно не так! Теперь, когда индексы включены, мой первый запрос на удаление занял 1: 9 час: мин (да, час!) по сравнению с 22:08 мин: сек и 13:21 мин. : сек по сравнению с 2:10 мин: сек для строк 42 мил и строк 13 мил соответственно. Сейчас я попробую выполнить запрос Тома с индексами, но он идет в неправильном направлении. Пожалуйста помоги.

Обновление (09.08.2010):

Удаление Тома заняло 1:06 ч: мин для строк 42 мил и 10:50 мин: сек для строк 13 мил с индексами по сравнению с 13:21 мин: сек и 2:13 мин: сек соответственно. Удаление в моей базе данных занимает больше времени, когда я использую индексы на порядок! Думаю, я знаю почему: размер моей базы данных .mdf и .ldf увеличился с 3,5 ГБ до 40,6 ГБ. Гб при первом (42 мил) удалении! Что я делаю не так?

Обновление (10.08.2010):

Из-за отсутствия каких-либо других вариантов я придумал, как мне кажется, тусклое решение (надеюсь, временное):

  1. Увеличьте время ожидания для подключения к базе данных до 1 часа (CommandTimeout=60000; по умолчанию было 30 секунд)
  2. Используйте запрос Тома: DELETE FROM WHERE EXISTS (SELECT 1 ...), потому что он работает немного быстрее
  3. DROP все индексы и PK перед выполнением оператора удаления (???)
  4. Запустить DELETE оператор
  5. CREATE все индексы и PK

Кажется сумасшествием, но, по крайней мере, это быстрее, чем использование TRUNCATE и начало моей нагрузки с самого начала с первого owner_id, потому что один из моих owner_id загружается за 2:30 ч: мин по сравнению с 17:22 мин: сек для процесса удаления. только что описанный с 42 миллионами строк. (Примечание: если мой процесс загрузки вызывает исключение, я начинаю с этого owner_id, но я не хочу сдуть предыдущий owner_id, поэтому я не хочу TRUNCATE таблицу owner_cats, поэтому я пытаюсь используйте DELETE.)

Мы будем благодарны за дополнительную помощь :)


person JohnB    schedule 06.08.2010    source источник
comment
Можете ли вы объяснить, что у вас есть для индексов в ваших таблицах?   -  person bobs    schedule 07.08.2010
comment
Я не кошатененавистник, но это не много ссор, зато много котов :) И, это меня разбивает Я не хочу отрывать кошек от других хозяев   -  person bobs    schedule 07.08.2010
comment
Это в базе данных CrazyOldLady?   -  person Dave Markle    schedule 07.08.2010
comment
Есть ли у столбцов owner_cats.id_cat, owner_cats.id_owner и cats.id_cat индексы? owner_cats.id_cat первичный ключ?   -  person Thomas    schedule 07.08.2010
comment
Как можно удалить строки при выборе одного владельца? Похоже, это всего несколько рядов. Это делает эти индексы очень важными. Я подозреваю, что вы сможете удалить кошек для одного владельца за пару секунд.   -  person bobs    schedule 07.08.2010
comment
@bobs: на самом деле у меня всего 2 владельца (может быть и больше, но сейчас у меня всего два хозяина с кучей кошек). В одном ряду 42 мил, в другом - 13 мил. Следовательно, у меня проблемы с удалением.   -  person JohnB    schedule 08.08.2010
comment
У меня нет ответа на ваш вопрос, но вы думали о стерилизации / стерилизации?   -  person Larry Lustig    schedule 10.08.2010
comment
@Thomas: при добавлении индексов в JOIN столбцы для DELETE SQL запросы DELETE выполняются намного дольше!   -  person JohnB    schedule 16.08.2010


Ответы (9)


Практического порога нет. Это зависит от того, какой тайм-аут вашей команды установлен в вашем соединении.

Имейте в виду, что время, необходимое для удаления всех этих строк, зависит от:

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

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

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

Что касается решения, ну, как и все, это зависит от обстоятельств. Вы собираетесь часто этим заниматься? В зависимости от того, сколько строк у вас осталось, самым быстрым способом может быть перестроение таблицы с другим именем, а затем ее переименование и воссоздание ограничений внутри транзакции. Если это просто специальная вещь, убедитесь, что ваш ADO CommandTimeout установлен достаточно высоким, и вы можете просто нести расходы на это большое удаление.

person Dave Markle    schedule 06.08.2010
comment
Что ж, у меня должно быть много споров по поводу этой коробки. Я не устанавливаю CommandTimeout, поэтому я использую значение по умолчанию 30 секунд. Кроме того, .ldf использует тот же диск, что и .mdf, но я, вероятно, смогу это изменить. Это процесс пакетной загрузки, и этот DELETE выполняется только тогда, когда время вызова веб-службы истекает, и мне нужно перезагрузить кошек только для владельца, которого я загружал в данный момент. - person JohnB; 07.08.2010

Если удаление приведет к удалению «значительного количества» строк из таблицы, это может быть альтернативой DELETE: поместить записи в другое место, обрезать исходную таблицу, вернуть «хранителей». Что-то типа:

SELECT *
INTO #cats_to_keep
FROM cats
WHERE cats.id_cat NOT IN (    -- note the NOT
SELECT owner_cats.id_cat FROM owner_cats
WHERE owner_cats.id_owner = 1)

TRUNCATE TABLE cats

INSERT INTO cats
SELECT * FROM #cats_to_keep
person Patrick Marchand    schedule 06.08.2010

Вы не пробовали использовать подзапросы и вместо этого использовали соединение?

DELETE cats 
FROM
 cats c
 INNER JOIN owner_cats oc
 on c.id_cat = oc.id_cat
WHERE
   id_owner =1

И если у вас есть, вы также пробовали разные подсказки присоединения, например.

DELETE cats 
FROM
 cats c
 INNER HASH JOIN owner_cats oc
 on c.id_cat = oc.id_cat
WHERE
   id_owner =1
person Conrad Frix    schedule 10.08.2010

Если вы используете EXISTS, а не IN, вы получите гораздо лучшую производительность. Попробуй это:

DELETE
  FROM cats c
 WHERE EXISTS (SELECT 1
                 FROM owner_cats o
                WHERE o.id_cat = c.id_cat
                  AND o.id_owner = 1)
person Tom    schedule 07.08.2010
comment
+1 помогает! С 42 миллионами строк, все еще без индексов, мой старый способ: 22: 8 мин: сек. Ваш путь: 13:21. Однако с 13 миллионами строк (у меня 2 владельца) мой старый способ: 2:10. Ваш путь: 2:13. Отличный совет, не могли бы вы объяснить, как это работает? - person JohnB; 07.08.2010
comment
Все дело в том, что оптимизатор действительно обрабатывает вещи, но в основном с предложением IN подвыбор должен быть полностью оценен, тогда как с EXISTS требуется только первая строка. - person Tom; 08.08.2010
comment
Без индексов вы всегда будете здесь напичканы. По крайней мере, вам нужно поместить индекс на owner_cats.id_cat, тогда это предложение EXISTS должно быть молниеносным. - person Tom; 08.08.2010
comment
Это сработало с небольшим изменением. Псевдоним не принимается во внешней таблице. - person Ravindra Gullapalli; 20.02.2014

Порогового значения как такового нет - вы можете УДАЛИТЬ все строки из любой таблицы, имея достаточно места в журнале транзакций - именно здесь ваш запрос, скорее всего, не выдержит. Если вы получаете какие-то результаты от вашего DELETE TOP (n) PERCENT FROM cats WHERE ... тогда вы можете заключить его в цикл, как показано ниже:

SELECT 1
WHILE @@ROWCOUNT <> 0
BEGIN
 DELETE TOP (somevalue) PERCENT FROM cats
 WHERE cats.id_cat IN (
 SELECT owner_cats.id_cat FROM owner_cats
 WHERE owner_cats.id_owner = 1)
END
person Will A    schedule 06.08.2010

Как уже упоминалось, когда вы удаляете 42 миллиона строк, база данных должна регистрировать 42 миллиона удалений в базе данных. Таким образом, журнал транзакций должен существенно увеличиться. Что вы можете попробовать, так это разбить удаление на части. В следующем запросе я использую функцию ранжирования NTile, чтобы разбить строки на 100 сегментов. Если это слишком медленно, вы можете увеличить количество сегментов, чтобы каждое удаление было меньше. Это очень поможет, если есть индекс для owner_cats.id_owner, owner_cats.id_cats и cats.id_cat (который я принял за первичный ключ и числовой).

Declare @Cats Cursor
Declare @CatId int  --assuming an integer PK here
Declare @Start int
Declare @End int
Declare @GroupCount int

Set @GroupCount = 100

Set @Cats = Cursor Fast_Forward For
    With CatHerd As
        (
        Select cats.id_cat
            , NTile(@GroupCount) Over ( Order By cats.id_cat ) As Grp
        From cats
            Join owner_cats
                On owner_cats.id_cat = cats.id_cat
        Where owner_cats.id_owner = 1
        )
        Select Grp, Min(id_cat) As MinCat, Max(id_cat) As MaxCat
        From CatHerd
        Group By Grp
Open @Cats
Fetch Next From @Cats Into @CatId, @Start, @End

While @@Fetch_Status = 0
Begin
    Delete cats
    Where id_cat Between @Start And @End

    Fetch Next From @Cats Into @CatId, @Start, @End
End 

Close @Cats
Deallocate @Cats

Примечательная особенность описанного выше подхода заключается в том, что он не является транзакционным. Таким образом, если произойдет сбой на 40-м фрагменте, вы удалите 40% строк, а остальные 60% все еще будут существовать.

person Thomas    schedule 06.08.2010
comment
Спасибо, возможно, мне придется попробовать это. Но что вы думаете о моей TOP (25) PERCENT идее? - person JohnB; 07.08.2010
comment
@John B - Обратной стороной решения TOP X% является то, что вам нужно запрашивать / переоценивать TOP X% на каждой итерации, а не только один раз, как я сделал здесь. - person Thomas; 07.08.2010

Возможно, стоит попробовать MERGE, например,

MERGE INTO cats 
   USING owner_cats
      ON cats.id_cat = owner_cats.id_cat
         AND owner_cats.id_owner = 1
WHEN MATCHED THEN DELETE;
person onedaywhen    schedule 30.09.2011
comment
Я не знал MERGE T-SQL. Спасибо за предложение; Я попробую и опубликую результаты, когда у меня будет возможность. - person JohnB; 30.09.2011

‹Edit› (28.09.2011)
Мой ответ работает в основном так же, как и решение Томаса (6 августа 2010 г.). Я пропустил это, когда опубликовал свой ответ, потому что он использует настоящий КУРСОР, поэтому я подумал про себя «плохо» из-за количества задействованных записей. Однако, когда я только что перечитал его ответ, я понял, что способ, которым он использует курсор, на самом деле «хорош». Очень умный. Я только что проголосовал за его ответ и, вероятно, воспользуюсь его подходом в будущем. Если вы не понимаете почему, взгляните на это еще раз. Если вы все еще не видите его, оставьте комментарий к этому ответу, и я вернусь и постараюсь подробно объяснить. Я решил оставить свой ответ, потому что у кого-то может быть администратор баз данных, который отказывается позволить им использовать настоящий КУРСОР, независимо от того, насколько он «хорош». :-)
‹/Edit›

Я понимаю, что этому вопросу уже год, но недавно у меня была похожая ситуация. Я пытался выполнить «массовое» обновление большой таблицы с присоединением к другой таблице, также довольно большой. Проблема заключалась в том, что объединение приводило к такому количеству «объединенных записей», что обработка занимала слишком много времени и могла привести к конфликтам. Поскольку это было разовое обновление, я придумал следующий «хак». Я создал WHILE LOOP, который прошел по таблице для обновления и выбрал 50 000 записей для обновления за раз. Выглядело это примерно так:

DECLARE @RecId bigint
DECLARE @NumRecs bigint
SET @NumRecs = (SELECT MAX(Id) FROM [TableToUpdate])
SET @RecId = 1
WHILE @RecId < @NumRecs
BEGIN
    UPDATE [TableToUpdate]
    SET UpdatedOn = GETDATE(),
        SomeColumn = t2.[ColumnInTable2]
    FROM    [TableToUpdate] t
    INNER JOIN [Table2] t2 ON t2.Name = t.DBAName 
        AND ISNULL(t.PhoneNumber,'') = t2.PhoneNumber 
        AND ISNULL(t.FaxNumber, '') = t2.FaxNumber
    LEFT JOIN [Address] d ON d.AddressId = t.DbaAddressId 
        AND ISNULL(d.Address1,'') = t2.DBAAddress1
        AND ISNULL(d.[State],'') = t2.DBAState
        AND ISNULL(d.PostalCode,'') = t2.DBAPostalCode
    WHERE t.Id BETWEEN @RecId AND (@RecId + 49999)
    SET @RecId = @RecId + 50000
END

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

‹Edit› (28.09.2011)
Есть ОГРОМНАЯ оговорка к предложению, которое упоминалось здесь более одного раза и публикуется повсюду в Интернете относительно копирования «хорошие» записи в другую таблицу, выполнение TRUNCATE (или DROP и reCREATE, или DROP и переименование), а затем повторное заполнение таблицы.

Вы не можете сделать это, если таблица является таблицей PK в отношении PK-FK (или другом ОГРАНИЧЕНИИ). Конечно, вы можете УБРАТЬ связь, выполнить очистку и восстановить связь, но вам также придется очистить таблицу FK. Вы можете сделать это ПЕРЕД восстановлением связи, что означает большее «время простоя», или вы можете выбрать НЕ ПРИМЕНЯТЬ ОГРАНИЧЕНИЕ при создании и очистить его после. Я думаю, вы также можете очистить таблицу FK ПЕРЕД очисткой таблицы PK. Суть в том, что вам нужно явно очистить таблицу FK, так или иначе.

Мой ответ - это гибридный процесс на основе SET / квази-КУРСОРА. Еще одним преимуществом этого метода является то, что если отношение PK-FK настроено на КАСКАДНОЕ УДАЛЕНИЕ, вам не нужно выполнять очистку, о которой я упоминал выше, потому что сервер позаботится об этом за вас. Если ваша компания или администратор баз данных не одобряют каскадное удаление, вы можете попросить включить его только во время выполнения этого процесса, а затем отключить, когда он будет завершен. В зависимости от уровней разрешений учетной записи, которая запускает очистку, операторы ALTER для включения / отключения каскадного удаления могут быть прикреплены к началу и концу оператора SQL. ‹/Edit›

person Andrew Steitz    schedule 04.08.2011

Ответ Билла Карвина на другой вопрос также относится к моей ситуации:

"Если ваша DELETE предназначена для удаления подавляющего большинства строк в этой таблице, люди часто делают то, что копируют только те строки, которые вы хотите сохранить, в дублирующую таблицу, а затем используют DROP TABLE или TRUNCATE, чтобы стереть исходную таблицу. намного быстрее ".

Мэтт в этом ответе говорит об этом так:

«В автономном режиме и при удалении большого% может иметь смысл просто создать новую таблицу с данными для хранения, отбросить старую таблицу и переименовать».

ammoQ в этом ответе (из того же вопроса) рекомендует (перефразировано) :

  • блокировать таблицу при удалении большого количества строк
  • поставить индексы на любые столбцы внешнего ключа
person JohnB    schedule 11.08.2010
comment
Проблема с предложениями Мэтта и Билла и аналогичными концепциями заключается в том, что я думаю, что копирование 42 миллионов строк может занять очень много времени. - person JohnB; 12.08.2010