Как максимально быстро обновить более 10 миллионов строк в отдельной таблице MySQL?

32

Использование MySQL 5.6 с механизмом хранения InnoDB для большинства таблиц. Размер пула буферов InnoDB составляет 15 ГБ, а индексы Innodb DB + - около 10 ГБ. Сервер имеет 32 ГБ оперативной памяти и работает под управлением Cent OS 7 x64.

У меня есть одна большая таблица, которая содержит около 10 миллионов записей.

Я получаю обновленный файл дампа с удаленного сервера каждые 24 часа. Файл в формате CSV. У меня нет контроля над этим форматом. Файл ~ 750 МБ. Я попытался вставить данные в таблицу MyISAM строка за строкой, и это заняло 35 минут.

Мне нужно взять только 3 значения на строку из 10-12 из файла и обновить его в базе данных.

Какой лучший способ добиться чего-то подобного?

Мне нужно делать это ежедневно.

В настоящее время поток выглядит так:

  1. mysqli_begin_transaction
  2. Читать файл дампа построчно
  3. Обновляйте каждую запись построчно.
  4. mysqli_commit

Вышеуказанные операции занимают около 30-40 минут, и при этом происходят другие обновления, которые дают мне

Превышено время ожидания блокировки; попробуйте перезапустить транзакцию

Обновление 1

загрузка данных в новую таблицу с использованием LOAD DATA LOCAL INFILE. В MyISAM это заняло, 38.93 secа в InnoDB - 7 минут 5,21 секунды. Тогда я сделал:

UPDATE table1 t1, table2 t2
SET 
t1.field1 = t2.field1,
t1.field2 = t2.field2,
t1.field3 = t2.field3
WHERE t1.field10 = t2.field10

Query OK, 434914 rows affected (22 hours 14 min 47.55 sec)

Обновление 2

то же самое обновление с запросом соединения

UPDATE table1 a JOIN table2 b 
ON a.field1 = b.field1 
SET 
a.field2 = b.field2,
a.field3 = b.field3,
a.field4 = b.field4

(14 hours 56 min 46.85 sec)

Разъяснения по вопросам в комментариях:

  • Приблизительно 6% строк в таблице будут обновлены файлом, но иногда это может достигать 25%.
  • Есть указатели на обновляемые поля. В таблице 12 индексов, и 8 индексов содержат поля обновления.
  • Нет необходимости делать обновление за одну транзакцию. Это может занять время, но не более 24 часов. Я хочу сделать это за 1 час, не блокируя всю таблицу, так как позже мне придется обновить индекс сфинкса, который зависит от этой таблицы. Неважно, если эти шаги занимают больше времени, если база данных доступна для других задач.
  • Я мог бы изменить формат CSV на этапе предварительной обработки. Единственное, что имеет значение, это быстрое обновление и без блокировки.
  • Таблица 2 - MyISAM. Это вновь созданная таблица из CSV-файла с использованием данных загрузки данных. Размер файла MYI составляет 452 МБ. Таблица 2 индексируется в столбце field1.
  • MYD таблицы MyISAM составляет 663 МБ.

Обновление 3:

здесь более подробно об обеих таблицах.

CREATE TABLE `content` (
  `hash` char(40) CHARACTER SET ascii NOT NULL DEFAULT '',
  `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `og_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `keywords` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `files_count` smallint(5) unsigned NOT NULL DEFAULT '0',
  `more_files` smallint(5) unsigned NOT NULL DEFAULT '0',
  `files` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '0',
  `category` smallint(3) unsigned NOT NULL DEFAULT '600',
  `size` bigint(19) unsigned NOT NULL DEFAULT '0',
  `downloaders` int(11) NOT NULL DEFAULT '0',
  `completed` int(11) NOT NULL DEFAULT '0',
  `uploaders` int(11) NOT NULL DEFAULT '0',
  `creation_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `upload_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `last_updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `vote_up` int(11) unsigned NOT NULL DEFAULT '0',
  `vote_down` int(11) unsigned NOT NULL DEFAULT '0',
  `comments_count` int(11) NOT NULL DEFAULT '0',
  `imdb` int(8) unsigned NOT NULL DEFAULT '0',
  `video_sample` tinyint(1) NOT NULL DEFAULT '0',
  `video_quality` tinyint(2) NOT NULL DEFAULT '0',
  `audio_lang` varchar(127) CHARACTER SET ascii NOT NULL DEFAULT '',
  `subtitle_lang` varchar(127) CHARACTER SET ascii NOT NULL DEFAULT '',
  `verified` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `uploader` int(11) unsigned NOT NULL DEFAULT '0',
  `anonymous` tinyint(1) NOT NULL DEFAULT '0',
  `enabled` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `tfile_size` int(11) unsigned NOT NULL DEFAULT '0',
  `scrape_source` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `record_num` int(11) unsigned NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`record_num`),
  UNIQUE KEY `hash` (`hash`),
  KEY `uploaders` (`uploaders`),
  KEY `tfile_size` (`tfile_size`),
  KEY `enabled_category_upload_date_verified_` (`enabled`,`category`,`upload_date`,`verified`),
  KEY `enabled_upload_date_verified_` (`enabled`,`upload_date`,`verified`),
  KEY `enabled_category_verified_` (`enabled`,`category`,`verified`),
  KEY `enabled_verified_` (`enabled`,`verified`),
  KEY `enabled_uploader_` (`enabled`,`uploader`),
  KEY `anonymous_uploader_` (`anonymous`,`uploader`),
  KEY `enabled_uploaders_upload_date_` (`enabled`,`uploaders`,`upload_date`),
  KEY `enabled_verified_category` (`enabled`,`verified`,`category`),
  KEY `verified_enabled_category` (`verified`,`enabled`,`category`)
) ENGINE=InnoDB AUTO_INCREMENT=7551163 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ROW_FORMAT=FIXED


CREATE TABLE `content_csv_dump_temp` (
  `hash` char(40) CHARACTER SET ascii NOT NULL DEFAULT '',
  `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `category_id` int(11) unsigned NOT NULL DEFAULT '0',
  `uploaders` int(11) unsigned NOT NULL DEFAULT '0',
  `downloaders` int(11) unsigned NOT NULL DEFAULT '0',
  `verified` tinyint(1) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`hash`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

и вот запрос на обновление, который обновляет contentтаблицу, используя данные изcontent_csv_dump_temp

UPDATE content a JOIN content_csv_dump_temp b 
ON a.hash = b.hash 
SET 
a.uploaders = b.uploaders,
a.downloaders = b.downloaders,
a.verified = b.verified

обновление 4:

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

mysql> UPDATE content_test a JOIN content_csv_dump_temp b
    -> ON a.hash = b.hash
    -> SET
    -> a.uploaders = b.uploaders,
    -> a.downloaders = b.downloaders,
    -> a.verified = b.verified;
Query OK, 2673528 rows affected (7 min 50.42 sec)
Rows matched: 7044818  Changed: 2673528  Warnings: 0

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

AMB
источник
У вас есть композит INDEX(field2, field3, field4) (в любом порядке)? Пожалуйста, покажите нам SHOW CREATE TABLE.
Рик Джеймс
1
12 и 8 индексы - это серьезная часть вашей проблемы. MyISAM - еще одна серьезная часть. InnoDB или TokuDB работают намного лучше с несколькими индексами.
Рик Джеймс
У вас есть два разных UPDATEs . Пожалуйста, расскажите нам , как выглядит прямое утверждение для обновления таблицы из данных CSV. Тогда мы сможем помочь вам разработать технику, которая соответствует вашим требованиям.
Рик Джеймс
@RickJames есть только один update, и, пожалуйста, проверьте обновленный вопрос. Спасибо
AMB

Ответы:

17

Исходя из моего опыта, я бы использовал LOAD DATA INFILE для импорта вашего CSV-файла.

Оператор LOAD DATA INFILE считывает строки из текстового файла в таблицу с очень высокой скоростью.

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

Пример таблицы

CREATE TABLE example (
  `Id` int(11) NOT NULL AUTO_INCREMENT,
  `Column2` varchar(14) NOT NULL,
  `Column3` varchar(14) NOT NULL,
  `Column4` varchar(14) NOT NULL,
  `Column5` DATE NOT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB

Пример CSV-файла

# more /tmp/example.csv
Column1,Column2,Column3,Column4,Column5
1,A,Foo,sdsdsd,4/13/2013
2,B,Bar,sdsa,4/12/2013
3,C,Foo,wewqe,3/12/2013
4,D,Bar,asdsad,2/1/2013
5,E,FOObar,wewqe,5/1/2013

Оператор импорта для запуска из консоли MySQL

LOAD DATA LOCAL INFILE '/tmp/example.csv'
    -> INTO TABLE example
    -> FIELDS TERMINATED BY ','
    -> LINES TERMINATED BY '\n'
    -> IGNORE 1 LINES
    -> (id, Column3,Column4, @Column5)
    -> set
    -> Column5 = str_to_date(@Column5, '%m/%d/%Y');

Результат

MySQL [testcsv]> select * from example;
+----+---------+---------+---------+------------+
| Id | Column2 | Column3 | Column4 | Column5    |
+----+---------+---------+---------+------------+
|  1 |         | Column2 | Column3 | 0000-00-00 |
|  2 |         | B       | Bar     | 0000-00-00 |
|  3 |         | C       | Foo     | 0000-00-00 |
|  4 |         | D       | Bar     | 0000-00-00 |
|  5 |         | E       | FOObar  | 0000-00-00 |
+----+---------+---------+---------+------------+

IGNORE просто игнорирует первую строку, которая является заголовком столбца.

После IGNORE мы указываем столбцы (пропускающие column2) для импорта, что соответствует одному из критериев в вашем вопросе.

Вот еще один пример непосредственно из Oracle: Пример LOAD DATA INFILE

Этого должно быть достаточно, чтобы вы начали.

Крейг Эфрейн
источник
Я мог бы использовать данные загрузки для загрузки данных во временную таблицу, а затем использовать другие запросы для обновления их в основной таблице. Спасибо
AMB
14

В свете всего вышесказанного, похоже, что узким местом является само соединение.

АСПЕКТ № 1: Размер буфера соединения

По всей вероятности, ваш join_buffer_size , вероятно, слишком низкий.

В соответствии с документацией MySQL о том, как MySQL использует буферный кеш объединения

Мы храним только используемые столбцы в буфере соединения, а не целые строки.

В этом случае ключи буфера соединения остаются в оперативной памяти.

У вас есть 10 миллионов строк по 4 байта для каждого ключа. Это около 40 млн.

Попробуйте увеличить его до 42M (чуть больше 40M)

SET join_buffer_size = 1024 * 1024 * 42;
UPDATE table1 a JOIN table2 b 
ON a.field1 = b.field1 
SET 
a.field2 = b.field2,
a.field3 = b.field3,
a.field4 = b.field4;

Если это помогает, продолжайте добавлять это к my.cnf

[mysqld]
join_buffer_size = 42M

Перезапуск mysqld не требуется для новых подключений. Просто беги

mysql> SET GLOBAL join_buffer_size = 1024 * 1024 * 42;

АСПЕКТ № 2: Операция соединения

Вы можете манипулировать стилем операции соединения, настраивая оптимизатор

Согласно документации MySQL по объединению блочно-циклического и пакетного доступа

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

Для использования BKA флаг batched_key_access системной переменной optimizer_switch должен быть включен. BKA использует MRR, поэтому флаг mrr также должен быть включен. В настоящее время оценка стоимости MRR слишком пессимистична. Следовательно, для использования BKA также необходимо отключить mrr_cost_based.

Эта же страница рекомендует сделать это:

mysql> SET optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';

АСПЕКТ № 3: Запись обновлений на диск (ДОПОЛНИТЕЛЬНО)

Большинство забывают увеличить innodb_write_io_threads, чтобы быстрее записывать грязные страницы из буферного пула.

[mysqld]
innodb_write_io_threads = 16

Вам придется перезапустить MySQL для этого изменения

ДАЙТЕ ЭТО ПОПРОБУЙТЕ !!!

RolandoMySQLDBA
источник
Ницца! +1 для настраиваемого кончика буфера соединения. Если вам нужно присоединиться, присоединяйтесь в памяти. Хороший совет!
Питер Диксон-Моисей
3
  1. CREATE TABLE что соответствует CSV
  2. LOAD DATA в этот стол
  3. UPDATE real_table JOIN csv_table ON ... SET ..., ..., ...;
  4. DROP TABLE csv_table;

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

Если больше ничего не пишет в таблицу, то ...

  1. CREATE TABLEэто соответствует CSV; нет индексов, кроме того, что необходимо в JOINв UPDATE. Если уникален, сделай это PRIMARY KEY.
  2. LOAD DATA в этот стол
  3. скопировать real_tableв new_table( CREATE ... SELECT)
  4. UPDATE new_table JOIN csv_table ON ... SET ..., ..., ...;
  5. RENAME TABLE real_table TO old, new_table TO real_table;
  6. DROP TABLE csv_table, old;

Шаг 3 выполняется быстрее, чем обновление, особенно если ненужные индексы не включены.
Шаг 5 «мгновенный».

Рик Джеймс
источник
скажем, в секундах, например, после шага 3 мы выполняем шаг 4, затем новые данные вставляются в real_table, поэтому мы пропустим эти данные в new_table? какой обходной путь для этого? спасибо
AMB
Посмотри что pt-online-schema-digest; он заботится о таких проблемах через TRIGGER.
Рик Джеймс
Вероятно, вам не нужны какие-либо индексы для таблицы из LOAD DATA. Добавление ненужных индексов обходится дорого (вовремя).
Рик Джеймс
Основываясь на последней информации, я склоняюсь к тому, что CSV-файл загружается в таблицу MyISAM с помощью всего лишь одного AUTO_INCREMENT, а затем разбивается на 1 000 строк за раз в зависимости от PK. Но мне нужно увидеть все требования и схему таблиц, прежде чем пытаться разобрать детали.
Рик Джеймс
Я установил хэш как PRIMARY index, но хотя порция в 50k с использованием запроса заказа занимает больше времени. Было бы лучше, если бы я создал автоинкремент? и установить это как PRIMARY index?
AMB
3

Вы сказали:

  • Обновления затрагивают 6-25% вашей таблицы
  • Вы хотите сделать это как можно быстрее (<1 час)
  • без блокировки
  • это не должно быть в одной транзакции
  • пока (комментируя ответ Рика Джеймса), вы выражаете беспокойство по поводу условий гонки

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

Кроме того, поскольку ваша таблица сильно проиндексирована, вставки и обновления могут выполняться медленно.


Избегать условий гонки

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

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

Вы избегаете условий гонки (при построчном обновлении), выполняя проверку того, что более позднее обновление еще не произошло ( UPDATE ... WHERE pk = [pk] AND updated < [batchfile date])

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


Работать как можно быстрее - распараллеливание

С этой отметкой времени теперь на месте:

  1. Разделите ваш пакетный файл на несколько частей разумного размера (скажем, 50000 строк / файл)
  2. Параллельно, прочитайте скрипт в каждом файле и выведите файл с 50 000 операторов UPDATE.
  3. Параллельно, как только (2) заканчивается, mysqlзапускается каждый файл sql.

(Например, в bashпоиске splitи xargs -Pдля способов простого запуска команды многими способами параллельно. Степень параллелизма зависит от того, сколько потоков вы готовы посвятить обновлению )

Питер Диксон-Моисей
источник
Имейте в виду, что «построчно», вероятно, будет в 10 раз медленнее, чем делать вещи партиями не менее 100.
Рик Джеймс
В этом случае вам нужно было бы проверить это, чтобы быть уверенным. Обновляя 6-25% таблицы (с 8 индексами, связанными с обновленными столбцами), я бы предположил, что обслуживание индекса становится узким местом.
Питер Диксон-Моисей
Я имею в виду, что в некоторых случаях может быть быстрее удалить индексы, выполнить массовое обновление и воссоздать их после ... но OP не хочет простоев.
Питер Диксон-Моисей
1

Большие обновления связаны с вводом / выводом. Я бы предложил:

  1. Создайте отдельную таблицу, в которой будут храниться ваши 3 часто обновляемых поля. Давайте назовем одну таблицу assets_static, в которой вы храните статические данные, а другую - assets_dynamic, которая будет хранить загрузчики, загрузчики и проверяться.
  2. Если вы можете, используйте механизм MEMORY для таблицы assets_dynamic . (резервное копирование на диск после каждого обновления).
  3. Обновите свой легкий и шустрый assets_dynamic в соответствии с вашим обновлением 4 (т. Е. LOAD INFILE ... INTO temp; UPDATE assets_dynamic a JOIN temp b на a.id = b.id SET [что должно быть обновлено]. Это должно занять меньше чем минута (в нашей системе assets_dynamic имеет 95 миллионов строк, а обновления влияют на ~ 6 миллионов строк, чуть более чем за 40 секунд ).
  4. Когда вы запускаете индексатор Sphinx, JOIN assets_static и assets_dynamic (при условии, что вы хотите использовать одно из этих полей в качестве атрибута).
user3127882
источник
0

Чтобы UPDATEбыстро бегать, вам нужно

INDEX(uploaders, downloaders, verified)

Это может быть на любом столе. Три поля могут быть в любом порядке.

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

И сделайте типы данных одинаковыми в двух таблицах (обе INT SIGNEDили обе INT UNSIGNED).

Рик Джеймс
источник
это на самом деле замедлило обновление.
AMB
Хммм ... Пожалуйста, предоставьте EXPLAIN UPDATE ...;.
Рик Джеймс