массовое обновление неиндексированного столбца в большой таблице InnoDB

У меня есть таблица InnoDB в базе данных Mysql 5.1, в которой около 27 миллионов строк. В этой таблице есть три неиндексированных столбца mediumint unsigned, которые я хочу периодически глобально сбрасывать на «0». Например:

update myTable set countA = 0;

Этот очень простой запрос на обновление сталкивается с проблемой блокировки InnoDB на уровне строк. После блокировки слишком большого количества строк запрос на обновление завершается ошибкой с хорошо задокументированной ошибкой:

ERROR 1206 (HY000): The total number of locks exceeds the lock table size

Проблема в том, что в такой большой таблице количество блокировок отдельных строк превысило пространство, выделенное для хранения блокировок.

Я нашел несколько советов, как справиться с этой проблемой:

Заблокируйте всю таблицу, чтобы отключите блокировку строк
Это показалось мне лучшим и самым чистым решением, и у меня нет проблем с тем, что эта конкретная таблица блокируется на несколько минут во время этих нечастых операций. Проблема в том, что данное решение на самом деле не сработало для меня. Может быть, это то, что используется для работы со старыми версиями Mysql?

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

Индексировать затронутые столбцы ( см. комментарии)
Если мы выполняем массовое обновление одного столбца, который поддерживается соответствующим индексом, то InnoDB может избежать блокировки всех строк. Используя индекс, он может заблокировать только затронутые строки. На самом деле я попробовал это, но обнаружил, что управление этими тремя индексами значительно замедляет мои добавочные обновления. Поскольку у меня будут десятки миллионов запросов на обновление, настраивающих эти три счетчика для каждого случая необходимости сброса счетчиков, я не хочу жертвовать эффективностью добавочных обновлений.

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

update myTable set countA = 0 where countA != 0 limit 500000;

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

Итак, какие возможности у меня остались?

  1. Прекратите использовать InnoDB: это потребует другой реорганизации моих текущих процессов, но я бы это рассмотрел.
  2. Переместите столбцы счетчика из основной таблицы: если у меня есть таблица CountA, я могу сбросить счетчики, используя delete from CountA, и я могу получить счетчики с помощью внутреннего соединения с основной таблицей. Это замедлило бы мои обновления отдельных счетчиков, поскольку мне пришлось бы получать идентификатор из основной таблицы перед условным обновлением или вставкой строки в таблицу CountA. Не супер, но что-то, что я бы рассмотрел.
  3. Что-то еще, что является одновременно чистым решением и тем, от которого можно ожидать достаточного роста с моими таблицами?

Обновление: с помощью принятого ответа у меня теперь есть реализация пакетной обработки, которая выполняет работу примерно за пять минут. Хотя я бы предпочел, чтобы пакетная обработка не была необходимой, пока не будет найдено более прямое решение. На случай, если это поможет следующему человеку наткнуться на этот вопрос, вот мой связанный код Java JDBC. (Рекомендуется также прочитать сообщение в блоге, связанное с принятым ответом.)

    int batchsize = 10_000;
    PreparedStatement pstmt = connection.prepareStatement
            ("UPDATE tableName SET countA = 0, countB = 0, countC = 0 "
                       + "WHERE id BETWEEN ? AND ?");
    for (int left = 0; left < maxId; left += batchsize) {
        pstmt.setInt(1, left + 1);
        pstmt.setInt(2, left + batchsize);
        pstmt.executeUpdate();
    }
    pstmt.close();

person frances    schedule 20.04.2015    source источник


Ответы (1)


План А

Мне нравится чанкинг (партирование). Однако ваш набросок кода не очень эффективен. Добавление OFFSET не помогает. Вместо этого посмотрите мой блог о тщательном просмотре таблицы. То есть найти «следующие» 100-1000 строк; выполнить UPDATE; петля. (Примечание: каждый фрагмент должен быть отдельной транзакцией.)

Техника «нахождения следующих N строк и запоминания того, где вы остановились» зависит от PRIMARY KEY. Мой блог охватывает большинство сценариев (числовых, строковых, разреженных и т. д.). (В блоге говорится о DELETE, но его легко адаптировать к UPDATE.)

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

План Б

Использование параллельной таблицы («переместить столбцы счетчика из основной таблицы»), вероятно, является хорошей идеей, потому что количество затрагиваемых дисковых блоков будет меньше, следовательно, это может быть похоже на план А, но быстрее. Используйте тот же PRIMARY KEY (без AUTO_INCREMENT).

План C

(1) параллельная таблица (например, Plan B), плюс (2) отсутствующая строка означает, что значения = 0. Затем очистка достигается с помощью TRUNCATE TABLE (в отличие от плана А). Поскольку вам нужно очистить три столбца, правила будут такими:

  • Когда какое-либо значение изменяется на ненулевое, убедитесь, что строка существует в параллельной таблице, и установите значение по мере необходимости (плюс нули для остальных). Вероятно, INSERT ... ON DUPLICATE KEY UPDATE... оптимально.
  • При поиске значений (SELECT) выполните LEFT JOIN и IFNULL(col, 0), чтобы получить значение или 0.

План X (не стартовый)

Индексирование столбцов может повредить. Когда вы ОБНОВЛЯЕТЕ индексированный столбец, необходимо изменить как данные, так и индекс.

person Rick James    schedule 21.04.2015
comment
Спасибо. Моя основная проблема с моей возможностью 2, которая соответствует тому, что вы описали в плане C, заключалась в том, что обновления будут медленными из-за необходимости просмотра основной таблицы для определения первичного ключа. использовать против параллельной таблицы, увеличивая время, затрачиваемое на обработку обновлений. В вашем ответе это не было затронуто, и я остановился на Плане А. После публикации вопроса я понял, что запрос BETWEEN, сопоставленный с моим полем id, будет лучше, но ваша запись в блоге помогла, наметив несколько перестановок. - person frances; 23.04.2015
comment
После небольшого тестирования я обнаружил, что партии из 10 000 в целом работают немного быстрее, чем партии из 1000, хотя мне пришлось бы провести гораздо больше тестов, чтобы определить идеальный размер партии и разницу. среди нескольких размеров партий, которые я пробовал, не настолько драматичен, чтобы я очень спешил возиться с ним дальше. - person frances; 23.04.2015
comment
И 1K, и 10K находятся в нижней части графика типа y=x*x; трудно найти точную точку минимума; достаточно хорошо является хорошей точкой остановки. - person Rick James; 24.04.2015