Обновление SQL занимает очень много времени / много времени занимает много часов

8

Да, это звучит как очень общая проблема, но я пока не смог ее сузить.

Итак, у меня есть оператор UPDATE в пакетном файле sql:

UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID

B имеет 40k записей, A имеет 4M записей, и они связаны 1-к-n через A.B_ID, хотя между ними нет FK.

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

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

DECLARE CursorB CURSOR FOR SELECT ID FROM B ORDER BY ID DESC -- Descending order
OPEN CursorB 
DECLARE @Id INT
FETCH NEXT FROM CursorB INTO @Id

WHILE @@FETCH_STATUS = 0
BEGIN
    DECLARE @Msg VARCHAR(50) = 'Updating A for B_ID=' + CONVERT(VARCHAR(10), @Id)
    RAISERROR(@Msg, 10, 1) WITH NOWAIT

    UPDATE A
    SET A.X = B.X
    FROM A JOIN B ON A.B_ID = B.ID
    WHERE B.ID = @Id

    FETCH NEXT FROM CursorB INTO @Id
END

Теперь я вижу, что он работает с сообщением с идентификатором по убыванию. Что происходит, это занимает около 5 минут, чтобы перейти от id = 40k к id = 13

А потом в id 13 почему-то, кажется, зависает. БД не имеет к ней никакого подключения, кроме SSMS, но на самом деле она не зависла:

  • жесткий диск работает постоянно, поэтому он определенно что-то делает (я проверил в Process Explorer, что это действительно процесс sqlserver.exe, использующий его)
  • Я запустил sp_who2, нашел SPID (70) сеанса SUSPENDED и запустил следующий скрипт:

    выберите * из sys.dm_exec_requests r присоединитесь к sys.dm_os_tasks t на r.session_id = t.session_id где r.session_id = 70

Это дает мне wait_type, который в большинстве случаев равен PAGEIOLATCH_SH, но на самом деле иногда меняется на WRITE_COMPLETION, что, как мне кажется, происходит, когда он очищает журнал

  • файл журнала, который был 1,6 ГБ, когда я восстановил БД (и когда он получил идентификатор 13), теперь 3,5 ГБ

Другая, может быть, полезная информация:

  • количество записей в таблице A для B_ID 13 невелико (14)
  • У моей коллеги нет той же проблемы на ее машине, с копией этой БД (от пары месяцев назад) с той же структурой.
  • таблица A является безусловно самой большой таблицей в БД
  • Он имеет несколько индексов, и несколько индексированных представлений используют его.
  • В БД нет другого пользователя, он локальный и ни одно приложение не использует его.
  • Файл LDF не ограничен в размере.
  • Модель восстановления ПРОСТА, уровень совместимости 100
  • Procmon не дает мне много информации: sqlserver.exe много читает и пишет из файлов MDF и LDF.

Я все еще жду, пока он закончится (это было 1:30), но я надеялся, что, возможно, кто-то даст мне какое-то другое действие, я мог бы попытаться устранить это.

Отредактировано: добавление выдержки из журнала procmon

15:24:02.0506105    sqlservr.exe    1760    ReadFile    C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA.mdf  SUCCESS Offset: 5,498,732,544, Length: 8,192, I/O Flags: Non-cached, Priority: Normal
15:24:02.0874427    sqlservr.exe    1760    WriteFile   C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA.mdf  SUCCESS Offset: 6,225,805,312, Length: 16,384, I/O Flags: Non-cached, Write Through, Priority: Normal
15:24:02.0884897    sqlservr.exe    1760    WriteFile   C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\TA_1.LDF    SUCCESS Offset: 4,589,289,472, Length: 8,388,608, I/O Flags: Non-cached, Write Through, Priority: Normal

При использовании DBCC PAGE кажется, что чтение и запись в поля выглядят как таблицы A (или один из ее индексов), но для другого B_ID это 13. Может быть, перестроение индексов?

Отредактировано 2: план выполнения

Поэтому я отменил запрос (фактически удалил базу данных и ее файлы, а затем восстановил ее) и проверил план выполнения:

UPDATE A
SET A.X = B.X
FROM A JOIN B ON A.B_ID = B.ID
WHERE B.ID = 13

(Предполагаемый) план выполнения такой же, как и для любого B.ID, и выглядит довольно просто. Предложение WHERE использует поиск индекса по некластеризованному индексу B, JOIN использует поиск кластеризованного индекса по обоим PK таблиц. Поиск кластеризованного индекса в A использует параллелизм (x7) и представляет 90% процессорного времени.

Что еще более важно, на самом деле выполнение запроса с ID 13 происходит немедленно.

Отредактировано 3: фрагментация индекса

Структура индексов следующая:

B имеет один кластеризованный PK (не поле идентификатора) и один некластеризованный уникальный индекс, первое поле которого - B.ID - этот второй индекс, кажется, используется всегда.

А имеет один кластерный ПК (поле не связано).

Есть также 7 представлений об A (все включают поле AX), каждое с собственным кластеризованным PK, а также другой индекс, который также включает поле AX

Представления фильтруются (с полями, не входящими в это уравнение), поэтому я сомневаюсь, что ОБНОВЛЕНИЕ А каким-либо образом будет использовать сами представления. Но у них есть индекс, включающий AX, поэтому изменение AX означает запись 7 представлений и 7 индексов, которые включают поле.

Хотя ожидается, что ОБНОВЛЕНИЕ будет медленнее, нет никаких причин, по которым конкретный идентификатор будет намного длиннее, чем другие.

Я проверил фрагментацию для всех индексов, все были на уровне <0,1%, кроме вторичных индексов просмотров , все между 25% и 50%. Коэффициенты заполнения для всех индексов, кажется, в порядке, между 90% и 95%.

Я реорганизовал все вторичные индексы и перезапустил свой скрипт.

Это все еще повешено, но в другой точке:

...
(0 row(s) affected)

        Updating A for B_ID=14

(4 row(s) affected)

Тогда как ранее журнал сообщений выглядел так:

...
(0 row(s) affected)

        Updating A for B_ID=14

(4 row(s) affected)

        Updating A for B_ID=13

Это странно, потому что это означает, что он даже не висит в одной и той же точке WHILEцикла. Остальное выглядит одинаково: та же самая строка UPDATE, ожидающая в sp_who2, та же самая ожидающая PAGEIOLATCH_EX и то же самое интенсивное использование HD от sqlserver.exe.

Следующий шаг - удалить все индексы и представления и воссоздать их, я думаю.

Отредактировано 4: удаление, а затем перестроение индексов

Итак, я удалил все проиндексированные представления, которые у меня были в таблице (7 из них, 2 индекса на представление, включая кластерное). Я запустил исходный скрипт (без курсора), и он фактически запустился за 5 минут.

Поэтому моя проблема связана с существованием этих индексов.

Я обновил свои индексы после запуска обновления, и это заняло 16 минут.

Теперь я понимаю, что для восстановления индексов требуется время, и я на самом деле в порядке с полной задачей, занимающей 20 минут.

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

GFK
источник
1
Что-нибудь в журнале ошибок SQL Server? Также от procmon, каковы смещения в файле, в который он пишет? Вы можете разделить на 8 192, чтобы получить страницу, а затем использовать, DBCC PAGEчтобы увидеть, что пишется.
Мартин Смит
3.5 ГБ выглядит как максимальный объем ОЗУ, который может обрабатывать 32-битная операционная система Windows .. опасность?
tschmit007
@MartinSmith Нет абсолютно ничего, так как я восстановил в журналах SSMS SQL Server и ничего в журнале событий Windows
GFK
Как выглядят ваши индексы в таблице A (какие столбцы и т. Д.)? Они фрагментированы?
Стюарт Эйнсворт
@ tschmit007 Выпуск SQL 2008 R2 x64 Dev Edition на Win Server 2008 R2 x64. Это виртуальная машина, работающая на Hyper-V (хост также 2008 R2 x64); виртуальная машина имеет 4,2 ГБ физической памяти, используемой из 5 ГБ, и 4,6 ГБ фиксации из 10 ГБ; хост имеет 7,2 ГБ физической памяти, используемой из 8 ГБ, и 7,8 коммитов из 16 ГБ макс. Обе машины работают медленнее из-за использования HD, но не забиты.
GFK

Ответы:

0
  1. Придерживайтесь команды UPDATE. Курсор будет медленнее, что вы пытаетесь сделать.
  2. Удалите / отключите все индексы, в том числе и для индексированных представлений. Если у вас есть внешний ключ на AX, сбросьте его.
  3. Создайте индекс, который будет содержать только A.B_ID, и еще один для B.ID.
  4. Даже если вы используете модель простого восстановления, последняя транзакция всегда будет в журнале транзакций, прежде чем она будет записана на диск. Вот почему вам необходимо предварительно увеличить журнал транзакций и настроить его на увеличение (например, 100 МБ).
  5. Кроме того, установите увеличение размера файла данных.
  6. Убедитесь, что у вас достаточно места на диске для дальнейшего роста файлов журнала и данных.
  7. После завершения обновления создайте заново / включите индексы, которые вы удалили / отключили на шаге 2.
  8. Если они вам больше не нужны, удалите индексы, созданные на шаге 3.

Изменить: Поскольку я не могу комментировать ваш исходный пост, я отвечу здесь на ваш вопрос из Edit 4. У вас есть 7 индексов на AX Index является B-деревом , и каждое обновление в этом поле вызывает перебалансировку B-дерева. Это быстрее восстановить эти индексы с нуля, чем каждый раз перебалансировать его.

боян
источник
По пункту 1 смотрите мой ответ на ik_zelf. Курсор существует для целей расследования и не оказывает такого большого влияния. Я собираюсь реализовать остальные ваши предложения, я думаю, что это все, что мне осталось сделать. Если это сработает, я все равно останусь без объяснения того, что происходит сейчас ...
GFK
Вы можете опубликовать DDL для своих таблиц (включая все индексы, ограничения и т. Д.). Возможно, что-то замедляет вашу производительность, и вы упускаете это.
Боян
1
Drop indexes / Update / Rebuild indexes работает, и хотя я предпочел бы не делать что-то радикальное, я не вижу, что у меня есть выбор. Спасибо!
GFK
0

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

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

То, что я хотел бы сделать, это запустить этот процесс с нуля с запущенным диспетчером задач. Если использование памяти превышает 75%, шансы того, что ваша система / процессы астрономически замерзнут.

Если ваша память / ресурсы действительно ограничены, как отмечено выше, тогда вы можете разделить процесс на более мелкие части (с периодической перезагрузкой, если использование памяти велико) вместо одной большой работы или обновить до 64-битного сервера с большим объемом памяти.

Технарь джо
источник
0

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

Поскольку вы обновляете столбец X всех строк в таблице A, убедитесь, что сначала вы удалили индекс для этой строки. Также убедитесь, что в этом столбце нет активных триггеров и ограничений.

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

ik_zelf
источник
Я не думаю, что в этом суть. Я понимаю, что обновление индексированных записей требует времени, и я знаю, что в целом часть времени, необходимая для этого, связана с этим. Но я ожидаю этого, и я согласен с этим: как я уже говорил, обновление 99% строк занимает 5 минут (даже с использованием курсора), но по какой-то причине одна строка (и не всегда одна и та же) занимает 5 часов. Меня беспокоит именно это поведение.
GFK
блокировка - это не проблема, которую вы сказали .... как насчет использования файловой системы, достигающей 90% или выше?
ik_zelf
нет, это 31 ГБ бесплатно из 120 ГБ, так что я думаю, что это нормально
GFK
что произойдет, если вы попытаетесь скопировать таблицу, например, создать таблицу a_copy как select * from a;
ik_zelf