Як оновити 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 і оновити його в базі даних.

Який найкращий спосіб досягти подібного?

Мені це потрібно робити щодня.

В даний час Flow такий:

  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 - 663MB.

Оновлення 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, оновиться, як тільки буде проведено розмітку на стенді.


Чи є у вас композит INDEX(field2, field3, field4) (у будь-якому порядку)? Будь ласка, покажіть нам SHOW CREATE TABLE.
Рік Джеймс

1
12 та 8 індексів є серйозною частиною вашої проблеми. MyISAM - ще одна серйозна частина. InnoDB або TokuDB працюють набагато краще з кількома індексами.
Рік Джеймс

У вас є дві різні UPDATEs . Скажіть, будь ласка , як саме виглядає прямий вислів для оновлення таблиці з даних CSV. Тоді ми можемо допомогти вам розробити техніку, яка відповідає вашим вимогам.
Рік Джеймс

@RickJames є тільки один update, і , будь ласка , перевірте оновлений питання, спасибі.
AMB

Відповіді:


17

Виходячи зі свого досвіду, я використовую ЗАВАНТАЖЕННЯ ДАНИХ 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 ми вказуємо стовпці (пропускаючи стовпчик2) для імпорту, що відповідає одному з критеріїв вашого запитання.

Ось ще один приклад безпосередньо з Oracle: Приклад завантаження даних INFILE

Цього має бути достатньо для початку роботи.


я міг би використовувати дані завантаження для завантаження даних у темп-таблиці, а потім використовувати інші запити, щоб оновити їх у головній таблиці., дякую
AMB

14

Зважаючи на всі згадані речі, схоже, вузьке місце - це саме з'єднання.

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

Ймовірно , ваш join_buffer_sizeЙмовірно , ймовірно, занадто низький.

Відповідно до документації MySQL про те, як MySQL використовує кеш буферу приєднання

Ми зберігаємо лише використані стовпці в буфері приєднання, а не цілі рядки.

У такому разі зробіть ключі буфера приєднання в оперативній пам'яті.

У вас є 10 мільйонів рядків разів 4 байти за кожен ключ. Це близько 40М.

Спробуйте зіткнути його на сеансі до 42М (трохи більше 40М)

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 для цієї зміни

СПРОБУВАТИ !!!


Приємно! +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К рядків за один раз на основі ПК. Але мені потрібно переглянути всі вимоги та схему таблиці, перш ніж спробувати деталізувати деталі.
Рік Джеймс

я встановив хеш як PRIMARY index, але хоч час збирання в 50 к за допомогою запиту на замовлення займає більше часу., було б краще, якби я створив автоматичне збільшення? і встановити це як PRIMARY index?
AMB

3

Ви сказали:

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

Багато з цих тверджень можуть бути суперечливими. Наприклад, великі оновлення без блокування таблиці. Або уникати перегонів без використання однієї гігантської транзакції.

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


Уникання перегонів

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

Це звільняє вас від оновлення по черзі (як зараз), але з автоматичним дорученням або більш розумними партіями транзакцій.

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

І, що важливо, це дозволяє запускати паралельні оновлення.


Біг як можна швидше - паралелізація

З цим часом позначка часу перевіряється:

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

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


Майте на увазі, що "рядок за рядком", ймовірно, буде в 10 разів повільніше, ніж робити речі партіями не менше 100.
Рік Джеймс

У цьому випадку вам доведеться порівняти його. Оновляючи 6-25% таблиці (з оновленими стовпцями задіяно 8 індексів), я розважаюсь над тим, що обслуговування індексів стає вузьким місцем.
Пітер Діксон-Мойсей

Я маю на увазі, в деяких випадках може бути швидше скинути індекси, масове оновлення та відтворити їх після ... але ОП не хоче простоїв.
Пітер Діксон-Мойсей

1

Великі оновлення пов'язані з введенням / виводу. Я б запропонував:

  1. Створіть окрему таблицю, в якій зберігатимуться 3 часто оновлювані поля. Давайте назвемо одну таблицю активів_статику, де ви зберігаєте, а також статичні дані, а іншу активу - динамічну, яка буде зберігати завантажувачів, завантажувачів і перевірених.
  2. Якщо ви можете, скористайтеся двигуном ПАМ’ЯТЬ для таблиці_надійної таблиці. (резервне копіювання на диск після кожного оновлення).
  3. Оновіть ваші легкі та спритні активи_динаміки відповідно до оновлення 4 (тобто ЗАВАНТАЖИТИ ІНФІЛЮВАННЯ ... НА ТЕМП; ОНОВЛЮВАТИ активи_динамічні ПРИЄДНАЙТЕ темп b на a.id = b.id SET [що має бути оновлено]. Це повинно зайняти менше хвилина. (У нашій системі, активність_динаміка є рядків і оновлення впливу - 6М рядків, трохи більше 40-х.)
  4. Коли ви запускаєте індексатор Sphinx, JOIN tools_static та properties_dynamic (якщо припустити, що ви хочете використовувати одне з цих полів як атрибут).

0

Для UPDATEшвидкого запуску вам потрібно

INDEX(uploaders, downloaders, verified)

Це може бути на будь-якому столі. Три поля можуть бути в будь-якому порядку.

Це полегшить UPDATEможливість швидко зіставити рядки між двома таблицями.

І зробіть типи даних однаковими у двох таблицях (обох INT SIGNEDабо обох INT UNSIGNED).


це фактично сповільнило оновлення.
AMB

Хммм ... Будь ласка, надайте EXPLAIN UPDATE ...;.
Рік Джеймс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.