Видалення мільйонів рядків у MySQL


79

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

  1. Напишіть сценарій, який буде виконувати тисячі менших запитів на видалення в циклі. Теоретично це дозволить обійти проблему заблокованої таблиці, оскільки інші запити зможуть потрапити до черги та виконуватися між видаленнями. Але це все одно буде значно збільшувати навантаження на базу даних і запускатиметься довго.
  2. Перейменуйте таблицю та відтворіть існуючу таблицю (вона тепер буде порожньою). Тоді зробіть мою прибирання на перейменованому столі. Перейменуйте нову таблицю, назвіть стару назад і об’єднайте нові рядки в перейменовану таблицю. Цей шлях робить значно більше кроків, але повинен виконувати роботу з мінімальними перервами. Єдина хитра частина тут полягає в тому, що дана таблиця є таблицею звітів, тому, як тільки вона буде перейменована, а порожня буде поставлена ​​на її місце, всі історичні звіти зникають, поки я не поверну її на місце. Плюс процес злиття може трохи зашкодити через тип даних, що зберігаються. Загалом, це мій вірогідний вибір зараз.

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



Нещодавно видалив близько 70 мільйонів записів в системі виробництва менш ніж за годину через збережені процедури, перевірте цю сторінку, це може допомогти іншим, а rathishkumar.in/2017/12 / ...
Rathish

Відповіді:


160
DELETE FROM `table`
WHERE (whatever criteria)
ORDER BY `id`
LIMIT 1000

Промити, промити, повторювати, поки не постраждають нульові ряди. Можливо, у сценарії, який пробуде секунду-три між ітераціями.


55
Якщо ви використовуєте DELETE з LIMIT, ви дійсно повинні використовувати ORDER BY, щоб зробити запит детермінованим; якщо цього не зробити, це матиме дивні наслідки (включаючи порушення реплікації в деяких випадках)
MarkR

6
Зверніть увагу, що не можна поєднувати DELETE ... JOIN з ORDER BYабо LIMIT.
єпископ

3
Я все ще сумніваюся, що зведена таблиця - не найкращий спосіб, але, я зробив процедуру, щоб у будь-якому разі зберегти розум: hastebin.com/nabejehure.pas
Діого Медейрос

Ось простий сценарій Python, який реалізує такий підхід: gist.github.com/tsauerwein/ffb159d1ab95d7fd91ef43b9609c471d
tsauerwein

9

У мене був випадок видалення 1M + рядків у таблиці 25M + рядків у MySQL. Спробував різні підходи, як пакетне видалення (описане вище).
Я виявив, що найшвидший спосіб (копія необхідних записів до нової таблиці):

  1. Створіть тимчасову таблицю, яка містить лише ідентифікатори.

СТВОРИТИ ТАБЛИЦЮ id_temp_table (temp_id int);

  1. Вставте ідентифікатори, які слід видалити:

вставити в id_temp_table (temp_id) вибрати .....

  1. Створити нову таблицю table_new

  2. Вставляйте всі записи з таблиці в таблицю_нову без зайвих рядків, що знаходяться в таблиці__темп

вставити в table_new .... де table_id НЕ ВСТАНОВИТИ (вибрати відмінний (temp_id) від id_temp_table);

  1. Перейменувати таблиці

Весь процес зайняв ~ 1 год. У моєму випадку простого видалення партії на 100 записів зайняло 10 хвилин.


для кроку 4 ви можете залишити об'єднання, щоб використовувати індекс: вставити в table_new ... вибрати ... з таблиці ліво join id_temp_table t на t.temp_id = table.id де t.temp_id NULL;
Softlion

8

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


7

наступне видаляє 1 000 000 записів по одному.

 for i in `seq 1 1000`; do 
     mysql  -e "select id from table_name where (condition) order by id desc limit 1000 " | sed 's;/|;;g' | awk '{if(NR>1)print "delete from table_name where id = ",$1,";" }' | mysql; 
 done

Ви можете згрупувати їх разом і видалити ім'я таблиці, де IN (id1, id2, .. idN) впевнені, що без особливих труднощів


1
Це єдине рішення, яке працювало для мене із таблицею на 100 Гб. Вибір з обмеженням 1000 становив лише кілька мілісекунд, але видалення з тим самим запитом зайняло годину лише для 1000 записів, хоча SSD є. Видалення цим способом все ще відбувається повільно, але не менше тисячі рядків на секунду, а не годину.
user2693017

видалення 1 М запису за один раз вб'є ваш сервер
спрощений

Я зміг одночасно видалити 100000 записів ( DELETE FROM table WHERE id <= 100000, потім 200000 тощо). Кожна партія займала від 30 секунд до 1 хвилини. Але коли я раніше намагався видалити відразу 1 300 000, запит виконувався щонайменше 30 хвилин, перш ніж провалився, ERROR 2013 (HY000): Lost connection to MySQL server during query.оскільки я запускав ці запити в клієнті MySQL на тій самій віртуальній машині, що і сервер, але, можливо, з’єднання минуло.
Buttle Butkus

3

Я б використав mk-archiver із чудового пакету службових програм Maatkit (купа сценаріїв Perl для управління MySQL). Maatkit від барона Шварца, автора книги "Висока продуктивність MySQL" O'Reilly.

Метою є робота з невеликим впливом, яка дозволяє перегризти старі дані з таблиці, не сильно впливаючи на запити OLTP. Ви можете вставити дані в іншу таблицю, яка не обов'язково повинна бути на тому самому сервері. Ви також можете записати його у файл у форматі, придатному для ЗАВАНТАЖЕННЯ ДАНИХ. Або ви не можете зробити ні те, ні інше, і в цьому випадку це просто поступове ВИДАЛЕННЯ.

Він уже створений для архівування ваших небажаних рядків невеликими партіями, і в якості бонусу він може зберегти видалені рядки у файл, якщо ви зіпсуєте запит, який вибирає рядки для видалення.

Інсталяція не потрібна, просто захопіть http://www.maatkit.org/get/mk-archiver і запустіть на ньому perldoc (або прочитайте веб-сайт) для отримання документації.


3

Ось рекомендована практика:

rows_affected = 0
do {
 rows_affected = do_query(
   "DELETE FROM messages WHERE created < DATE_SUB(NOW(),INTERVAL 3 MONTH)
   LIMIT 10000"
 )
} while rows_affected > 0

Видалення 10000 рядків за раз, як правило, є досить великим завданням, щоб зробити кожен запит ефективним, і досить коротким завданням, щоб мінімізувати вплив на сервер4 (механізми зберігання транзакцій можуть отримати користь від менших транзакцій). Також може бути гарною ідеєю додати деякий час сну між операторами DELETE, щоб розподілити навантаження з часом і зменшити кількість часу утримання блокування.

Посилання на MySQL High Performance


2

Для нас DELETE WHERE %s ORDER BY %s LIMIT %dвідповідь не був варіантом, оскільки критерій WHERE був повільним (неіндексований стовпець), і він би вдарив майстра.

ВИБЕРІТЬ із читання-репліки список первинних ключів, які потрібно видалити. Експорт у такому форматі:

00669163-4514-4B50-B6E9-50BA232CA5EB
00679DE5-7659-4CD4-A919-6426A2831F35

Використовуйте наступний скрипт bash, щоб захопити цей вхід і поділити його на оператори DELETE [вимагає bash ≥ 4 через mapfileвбудований ]:

sql-chunker.sh (пам'ятайте про chmod +xмене і змініть shebang, щоб вказати на ваш виконуваний файл bash 4) :

#!/usr/local/Cellar/bash/4.4.12/bin/bash

# Expected input format:
: <<!
00669163-4514-4B50-B6E9-50BA232CA5EB
00669DE5-7659-4CD4-A919-6426A2831F35
!

if [ -z "$1" ]
  then
    echo "No chunk size supplied. Invoke: ./sql-chunker.sh 1000 ids.txt"
fi

if [ -z "$2" ]
  then
    echo "No file supplied. Invoke: ./sql-chunker.sh 1000 ids.txt"
fi

function join_by {
    local d=$1
    shift
    echo -n "$1"
    shift
    printf "%s" "${@/#/$d}"
}

while mapfile -t -n "$1" ary && ((${#ary[@]})); do
    printf "DELETE FROM my_cool_table WHERE id IN ('%s');\n" `join_by "','" "${ary[@]}"`
done < "$2"

Закликайте так:

./sql-chunker.sh 1000 ids.txt > batch_1000.sql

Це дасть вам файл із вихідним форматом, відформатованим приблизно так (я використовував пакетний розмір 2):

DELETE FROM my_cool_table WHERE id IN ('006CC671-655A-432E-9164-D3C64191EDCE','006CD163-794A-4C3E-8206-D05D1A5EE01E');
DELETE FROM my_cool_table WHERE id IN ('006CD837-F1AD-4CCA-82A4-74356580CEBC','006CDA35-F132-4F2C-8054-0F1D6709388A');

Потім виконайте твердження так:

mysql --login-path=master billing < batch_1000.sql

Для незнайомих login-pathце просто ярлик для входу без введення пароля в командному рядку.


2

Я зіткнувся з подібною проблемою. У нас була справді велика таблиця, розміром близько 500 ГБ без розділів та одним лише індексом у стовпці primary_key. Наш господар був кузовом машини, 128 ядер і 512 гіг оперативної пам'яті, і у нас теж було кілька рабів. Ми спробували кілька прийомів для вирішення масштабного видалення рядків. Я перелічу їх усіх тут від найгіршого до найкращого, що ми знайшли -

  1. Отримання та видалення по одному рядку за раз. Це абсолютно найгірше, що ви могли зробити. Отже, ми навіть цього не пробували.
  2. Отримання перших рядків "X" з бази даних за допомогою обмеженого запиту в стовпці primary_key, а потім перевірка ідентифікаторів рядків, які потрібно видалити в програмі, та виклик одного запиту на видалення зі списком ідентифікаторів primary_key. Отже, 2 запити на рядки "X". Тепер цей підхід був нормальним, але за допомогою пакетного завдання за 10 хвилин близько 10 хвилин було видалено близько 5 мільйонів рядків, завдяки чому підлеглі нашої БД MySQL відставали на 105 секунд. 105-секундне відставання у 10-хвилинній активності. Отже, нам довелося зупинитися.
  3. У цій техніці ми ввели затримку в 50 мс між нашим наступним завантаженням партії та видаленнями кожного розміру "X". Це вирішило проблему відставання, але ми тепер видаляли 1,2-1,3 мільйона рядків за 10 хвилин порівняно з 5 мільйонами в техніці №2.
  4. Розбиття таблиці на базу даних, а потім видалення цілих розділів, коли це не потрібно. Це найкраще рішення, яке ми маємо, але воно вимагає попередньо розділеної таблиці. Ми виконали крок 3, оскільки у нас була несекціонована дуже стара таблиця з індексацією лише у стовпці primary_key. Створення розділу зайняло б занадто багато часу, і ми перебували в кризовому режимі. Ось кілька посилань, пов’язаних із секціонуванням, які я знайшов корисними - Офіційний довідник MySQL , Щоденне розділення Oracle DB .

Отже, IMO, якщо ви можете дозволити собі розкіш створити розділ у своїй таблиці, перейдіть за варіантом №4, інакше ви застрягли у варіанті №3.


1

Робіть це партіями, скажімо, 2000 рядків одночасно. Здійснюйте посеред. Мільйон рядків - це не так вже й багато, і це буде швидко, якщо у вас у таблиці не багато індексів.


1

Я думаю, що повільність обумовлена ​​"кластерним індексом" MySQl, де фактичні записи зберігаються в індексі первинного ключа - в порядку індексу первинного ключа. Це означає, що доступ до запису через первинний ключ надзвичайно швидкий, оскільки для цього потрібен лише один вибір диска, оскільки запис на диску прямо там, де він знайшов правильний первинний ключ в індексі.

В інших базах даних без кластеризованих індексів сам індекс не містить запису, а просто "зміщення" або "розташування", що вказує, де запис знаходиться у файлі таблиці, а потім у цьому файлі потрібно зробити другу вибірку для отримання фактичних даних .

Ви можете собі уявити, видаляючи запис у кластеризованому індексі, що всі записи над цим записом у таблиці повинні бути переміщені вниз, щоб уникнути масивних дірок в індексі (ну саме це я пам’ятаю ще принаймні кілька років тому - пізніші версії можливо це змінило).

Знаючи вищесказане, ми виявили, що дійсно прискорене видалення в MySQL мало виконувати видалення в зворотному порядку. Це створює найменшу кількість переміщення записів, оскільки ви видаляєте записи з кінця першого, що означає, що наступні видалення мають менше об’єктів для переміщення.


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

0

У мене була справді завантажена база, яка постійно потребувала видалення деяких старих записів. Деякі запити на видалення почали зависати, тому мені потрібно було їх убити, і якщо їх буде занадто багато, вся база стає неактивною, тому мені потрібно було обмежити паралельні прогони. Тож я створив cron jobзапущений щохвилини, починаючи цей сценарій:

#!/bin/bash

#######################
#
i_size=1000
max_delete_queries=10
sleep_interval=15
min_operations=8
max_query_time=1000

USER="user"
PASS="super_secret_password"

log_max_size=1000000
log_file="/var/tmp/clean_up.log"
#
#######################

touch $log_file
log_file_size=`stat -c%s "$log_file"`
if (( $log_file_size > $log_max_size ))
then
    rm -f "$log_file"
fi 

delete_queries=`mysql -u user -p$PASS -e  "SELECT * FROM information_schema.processlist WHERE Command = 'Query' AND INFO LIKE 'DELETE FROM big.table WHERE result_timestamp %';"| grep Query|wc -l`

## -- here the hanging DELETE queries will be stopped
mysql-u $USER -p$PASS -e "SELECT ID FROM information_schema.processlist WHERE Command = 'Query' AND INFO LIKE 'DELETE FROM big.table WHERE result_timestamp %'and TIME>$max_query_time;" |grep -v ID| while read -r id ; do
    echo "delete query stopped on `date`" >>  $log_file
    mysql -u $USER -p$PASS -e "KILL $id;"
done

if (( $delete_queries > $max_delete_queries ))
then
  sleep $sleep_interval

  delete_queries=`mysql-u $USER -p$PASS -e  "SELECT * FROM information_schema.processlist WHERE Command = 'Query' AND INFO LIKE 'DELETE FROM big.table WHERE result_timestamp %';"| grep Query|wc -l`

  if (( $delete_queries > $max_delete_queries ))
  then

      sleep $sleep_interval

      delete_queries=`mysql -u $USER -p$PASS -e  "SELECT * FROM information_schema.processlist WHERE Command = 'Query' AND INFO LIKE 'DELETE FROM big.table WHERE result_timestamp %';"| grep Query|wc -l`

      # -- if there are too many delete queries after the second wait
      #  the table will be cleaned up by the next cron job
      if (( $delete_queries > $max_delete_queries ))
        then
            echo "clean-up skipped on `date`" >> $log_file
            exit 1
        fi
  fi

fi

running_operations=`mysql-u $USER -p$PASS -p -e "SELECT * FROM INFORMATION_SCHEMA.PROCESSLIST WHERE COMMAND != 'Sleep';"| wc -l`

if (( $running_operations < $min_operations ))
then
    # -- if the database is not too busy this bigger batch can be processed
    batch_size=$(($i_size * 5))
else 
    batch_size=$i_size
fi

echo "starting clean-up on `date`" >>  $log_file

mysql-u $USER -p$PASS -e 'DELETE FROM big.table WHERE result_timestamp < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 31 DAY))*1000 limit '"$batch_size"';'

if [ $? -eq 0 ]; then
    # -- if the sql command exited normally the exit code will be 0
    echo "delete finished successfully on `date`" >>  $log_file
else
    echo "delete failed on `date`" >>  $log_file
fi

Завдяки цьому я добився близько 2 мільйонів видалень на день, що було нормально для мого випадку.


0

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

Раніше я використовував тимчасову таблицю для ідентифікації записів, які слід видалити.

Тимчасова таблиця, яку я використовував 'archive_temp' для зберігання ідентифікаторів, створених у пам'яті, без будь-яких індексів.

Отже, під час видалення записів з оригінальної таблиці транзакцій, наприклад, ВИДАЛИТИ з tat, де id в (виберіть id з archive_temp); запит, що використовується для повернення помилки "LOST Connection to server"

Я створив індекс для цієї тимчасової таблиці наступним чином після її створення: ALTER TABLE archive_tempADD INDEX ( id);

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

Отже, було б краще перевірити індекси. Сподіваюся, це може допомогти.


-3

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

Це (очевидно) вимагає багато додаткового дискового простору, і це може оподаткувати ваші ресурси вводу-виводу, але в іншому випадку це може бути набагато швидше.

Залежно від характеру даних або в надзвичайних ситуаціях, ви можете перейменувати стару таблицю і створити на ній нову, порожню таблицю, а на дозвіллі вибрати рядки "зберегти" в новій таблиці ...


-6

Згідно з документацією mysql , TRUNCATE TABLEце швидка альтернатива DELETE FROM. Спробуйте це:

ЗМІНИТИ ТАБЛИЦЮ ім'я таблиці

Я спробував це на 50 мільйонах рядків, і це було зроблено протягом двох хвилин.

Примітка: Операції зрізання не безпечні для транзакцій; помилка виникає під час спроби такої дії під час активної транзакції або активного блокування таблиці


26
Це однозначно видалить рядки. Я майже впевнений, що ОП хоче бути вибірковим.
adam rowe
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.