Более быстрый способ удаления совпадающих строк? - PullRequest
57 голосов
/ 01 мая 2009

Я относительный новичок, когда дело доходит до баз данных. Мы используем MySQL, и в настоящее время я пытаюсь ускорить выполнение оператора SQL, который, похоже, требует времени для запуска. Я посмотрел вокруг на SO на похожий вопрос, но не нашел.

Цель состоит в том, чтобы удалить все строки в таблице A, которые имеют совпадающий идентификатор в таблице B.

В настоящее время я делаю следующее:

DELETE FROM a WHERE EXISTS (SELECT b.id FROM b WHERE b.id = a.id);

В таблице a приблизительно 100 000 строк и около 22 000 строк в таблице b. Столбец id - это PK для обеих таблиц.

Это утверждение занимает около 3 минут для запуска на моем тестовом компьютере - Pentium D, XP SP3, 2 ГБ оперативной памяти, MySQL 5.0.67. Это кажется медленным для меня. Возможно, это не так, но я надеялся ускорить процесс. Есть ли лучший / более быстрый способ сделать это?


EDIT:

Некоторая дополнительная информация, которая может оказаться полезной. Таблицы A и B имеют ту же структуру, что и я, для создания таблицы B я сделал следующее:

CREATE TABLE b LIKE a;

В таблице a (и, следовательно, в таблице b) есть несколько индексов, помогающих ускорить выполнение запросов к ней. Опять же, я относительный новичок в работе с БД и все еще учусь. Я не знаю, какое влияние это оказывает на вещи. Я предполагаю, что это имеет эффект, так как индексы тоже должны быть очищены, верно? Мне также было интересно, есть ли другие параметры БД, которые могут повлиять на скорость.

Также я использую INNO DB.


Вот дополнительная информация, которая может быть вам полезна.

Таблица A имеет подобную структуру (я немного продезинфицировал ее):

DROP TABLE IF EXISTS `frobozz`.`a`;
CREATE TABLE  `frobozz`.`a` (
  `id` bigint(20) unsigned NOT NULL auto_increment,
  `fk_g` varchar(30) NOT NULL,
  `h` int(10) unsigned default NULL,
  `i` longtext,
  `j` bigint(20) NOT NULL,
  `k` bigint(20) default NULL,
  `l` varchar(45) NOT NULL,
  `m` int(10) unsigned default NULL,
  `n` varchar(20) default NULL,
  `o` bigint(20) NOT NULL,
  `p` tinyint(1) NOT NULL,
  PRIMARY KEY  USING BTREE (`id`),
  KEY `idx_l` (`l`),
  KEY `idx_h` USING BTREE (`h`),
  KEY `idx_m` USING BTREE (`m`),
  KEY `idx_fk_g` USING BTREE (`fk_g`),
  KEY `fk_g_frobozz` (`id`,`fk_g`),
  CONSTRAINT `fk_g_frobozz` FOREIGN KEY (`fk_g`) REFERENCES `frotz` (`g`)
) ENGINE=InnoDB AUTO_INCREMENT=179369 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

Я подозреваю, что отчасти проблема в том, что для этой таблицы есть несколько индексов. Таблица B выглядит аналогично таблице B, хотя она содержит только столбцы id и h.

Кроме того, результаты профилирования следующие:

starting 0.000018
checking query cache for query 0.000044
checking permissions 0.000005
Opening tables 0.000009
init 0.000019
optimizing 0.000004
executing 0.000043
end 0.000005
end 0.000002
query end 0.000003
freeing items 0.000007
logging slow query 0.000002
cleaning up 0.000002

решаемые

Спасибо всем за отзывы и комментарии. Они, конечно, заставили меня задуматься о проблеме. Слава dotjoe за то, что я заставил меня отойти от проблемы, задав простой вопрос "Есть ли в других таблицах ссылка на a.id?"

?"

Проблема заключалась в том, что в таблице A был DELETE TRIGGER, который вызывал хранимую процедуру для обновления двух других таблиц, C и D. В таблице C был FK обратно в a.id, и после выполнения некоторых действий, связанных с этим идентификатором, в хранимая процедура, это было утверждение,

DELETE FROM c WHERE c.id = theId;

Я посмотрел на оператор EXPLAIN и переписал это как

EXPLAIN SELECT * FROM c WHERE c.other_id = 12345;

Итак, я мог видеть, что это делает, и это дало мне следующую информацию:

id            1
select_type   SIMPLE
table         c
type          ALL
possible_keys NULL
key           NULL
key_len       NULL
ref           NULL
rows          2633
Extra         using where

Это говорило мне, что это была болезненная операция, и, поскольку она собиралась вызываться 22500 раз (для данного набора данных, которые были удалены), это была проблема. После того, как я создал INDEX для этого столбца other_id и перезапустил EXPLAIN, я получил:

id            1
select_type   SIMPLE
table         c
type          ref
possible_keys Index_1
key           Index_1
key_len       8
ref           const
rows          1
Extra         

Гораздо лучше, на самом деле действительно здорово.

Я добавил, что Index_1 и мое время удаления соответствуют временам, указанным mattkemp . С моей стороны это было очень тонкой ошибкой из-за того, что в последнюю минуту были добавлены дополнительные функции. Оказалось, что большинство предложенных альтернативных операторов DELETE / SELECT, как заявил Даниэль , в конечном итоге заняли, по существу, такое же количество времени, и, как упоминалось soulmerge , это заявление было в значительной степени Лучше всего я смогу построить на основе того, что мне нужно было сделать. Как только я предоставил индекс для этой другой таблицы C, мои DELETE были быстрыми.

Посмертное
Из этого упражнения извлечены два урока. Во-первых, ясно, что я не использовал возможности оператора EXPLAIN, чтобы лучше понять влияние моих SQL-запросов. Это ошибка новичка, так что я не собираюсь изводить себя этим. Я учусь на этой ошибке. Во-вторых, нарушающий код был результатом менталитета «сделай это быстро», а неадекватный дизайн / тестирование привели к тому, что эта проблема не появилась раньше. Если бы я сгенерировал несколько значительных наборов тестовых данных для использования в качестве входных данных теста для этой новой функциональности, я бы не потратил впустую ни свое, ни ваше. Моему тестированию на стороне БД не хватало глубины, которую имеет моя прикладная сторона. Теперь у меня есть возможность улучшить это.

Ссылка: ОБЪЯСНИТЕ Заявление

Ответы [ 14 ]

74 голосов
/ 06 мая 2009

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

Хотя может быть трудно понять, почему УДАЛЕНИЯ во всех случаях являются самыми медленными, есть довольно простое объяснение. InnoDB - это транзакционный механизм хранения. Это означает, что если ваш запрос был прерван на полпути, все записи были бы на месте, как будто ничего не произошло. Как только оно будет завершено, все исчезнет в одно мгновение. Во время УДАЛЕНИЯ другие клиенты, подключающиеся к серверу, будут видеть записи, пока ваше УДАЛЕНИЕ не завершится.

Чтобы достичь этого, InnoDB использует технику, называемую MVCC (Multi Version Concurrency Control). По сути, он дает каждому соединению моментальный снимок всей базы данных, как это было при запуске первого оператора транзакции. Для этого каждая внутренняя запись InnoDB может иметь несколько значений - по одному для каждого снимка. Именно поэтому COUNTing на InnoDB занимает некоторое время - это зависит от состояния моментального снимка, который вы видите в это время.

Для вашей транзакции DELETE каждая запись, идентифицированная в соответствии с условиями вашего запроса, помечается для удаления. Поскольку другие клиенты могут получать доступ к данным в одно и то же время, они не могут немедленно удалить их из таблицы, потому что им нужно увидеть соответствующий снимок, чтобы гарантировать атомарность удаления.

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

Так что на самом деле ваши 3 минуты не такие уж и медленные, учитывая тот факт, что все записи должны быть изменены, чтобы подготовить их к удалению безопасным способом транзакции. Возможно, вы «услышите», как работает ваш жесткий диск во время выполнения инструкции. Это вызвано доступом ко всем строкам. Чтобы повысить производительность, вы можете попытаться увеличить размер пула буферов InnoDB для вашего сервера и попытаться ограничить другой доступ к базе данных во время удаления, тем самым уменьшив количество исторических версий, которые InnoDB должен поддерживать для каждой записи. С дополнительной памятью InnoDB может читать вашу таблицу (в основном) в память и избегать некоторого времени поиска диска.

9 голосов
/ 06 мая 2009

Ваше время в три минуты кажется очень медленным. Я предполагаю, что столбец id не индексируется должным образом. Если бы вы могли предоставить точное определение таблицы, которую вы используете, это было бы полезно.

Я создал простой скрипт на python для получения тестовых данных и запустил несколько разных версий запроса на удаление одного и того же набора данных. Вот мои определения таблицы:

drop table if exists a;
create table a
 (id bigint unsigned  not null primary key,
  data varchar(255) not null) engine=InnoDB;

drop table if exists b;
create table b like a;

Затем я вставил 100k строк в a и 25k строк в b (22.5k из которых также были в a). Вот результаты различных команд удаления. Между прочим, я уронил и снова заполнил стол.

mysql> DELETE FROM a WHERE EXISTS (SELECT b.id FROM b WHERE a.id=b.id);
Query OK, 22500 rows affected (1.14 sec)

mysql> DELETE FROM a USING a LEFT JOIN b ON a.id=b.id WHERE b.id IS NOT NULL;
Query OK, 22500 rows affected (0.81 sec)

mysql> DELETE a FROM a INNER JOIN b on a.id=b.id;
Query OK, 22500 rows affected (0.97 sec)

mysql> DELETE QUICK a.* FROM a,b WHERE a.id=b.id;
Query OK, 22500 rows affected (0.81 sec)

Все тесты проводились на четырехъядерном процессоре Intel Core2 с тактовой частотой 2,5 ГГц, оперативной памятью 2 ГБ с Ubuntu 8.10 и MySQL 5.0. Обратите внимание, что выполнение одного оператора sql все еще однопоточное.


Обновление:

Я обновил свои тесты, чтобы использовать схему itsmatt. Я немного изменил его, удалив автоинкремент (я генерирую синтетические данные) и кодировку набора символов (не работал - не копался в этом).

Вот мои новые определения таблиц:

drop table if exists a;
drop table if exists b;
drop table if exists c;

create table c (id varchar(30) not null primary key) engine=InnoDB;

create table a (
  id bigint(20) unsigned not null primary key,
  c_id varchar(30) not null,
  h int(10) unsigned default null,
  i longtext,
  j bigint(20) not null,
  k bigint(20) default null,
  l varchar(45) not null,
  m int(10) unsigned default null,
  n varchar(20) default null,
  o bigint(20) not null,
  p tinyint(1) not null,
  key l_idx (l),
  key h_idx (h),
  key m_idx (m),
  key c_id_idx (id, c_id),
  key c_id_fk (c_id),
  constraint c_id_fk foreign key (c_id) references c(id)
) engine=InnoDB row_format=dynamic;

create table b like a;

Затем я перезапускаю те же тесты со строками 100k в строках a и 25k в b (и переполняю их между выполнениями).

mysql> DELETE FROM a WHERE EXISTS (SELECT b.id FROM b WHERE a.id=b.id);
Query OK, 22500 rows affected (11.90 sec)

mysql> DELETE FROM a USING a LEFT JOIN b ON a.id=b.id WHERE b.id IS NOT NULL;
Query OK, 22500 rows affected (11.48 sec)

mysql> DELETE a FROM a INNER JOIN b on a.id=b.id;
Query OK, 22500 rows affected (12.21 sec)

mysql> DELETE QUICK a.* FROM a,b WHERE a.id=b.id;
Query OK, 22500 rows affected (12.33 sec)

Как видите, это немного медленнее, чем раньше, возможно, из-за множества индексов. Тем не менее, это далеко не три минуты.

Что-то еще, на что вы могли бы взглянуть, - это перемещение поля длинного текста в конец схемы. Кажется, я помню, что mySQL работает лучше, если все поля с ограниченным размером являются первыми, а text, blob и т. Д. - в конце.

8 голосов
/ 01 мая 2009

Попробуйте это:

DELETE a
FROM a
INNER JOIN b
 on a.id = b.id

Использование подзапросов, как правило, медленнее, чем объединений, поскольку они запускаются для каждой записи во внешнем запросе.

5 голосов
/ 07 мая 2009

Это то, что я всегда делаю, когда мне приходится работать со сверхбольшими данными (здесь: примерная тестовая таблица с 150000 строками):

drop table if exists employees_bak;
create table employees_bak like employees;
insert into employees_bak 
    select * from employees
    where emp_no > 100000;

rename table employees to employees_todelete;
rename table employees_bak to employees;

В этом случае sql фильтрует 50000 строк в таблицу резервных копий. Каскад запросов выполняется на моей медленной машине за 5 секунд. Вы можете заменить вставку в select вашим собственным фильтром запроса.

Это хитрость для массового удаления в больших базах данных!; =)

3 голосов
/ 10 мая 2009

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

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

Итак, вот оно: сначала выполните SELECT, как отдельный запрос , запоминая идентификаторы, возвращенные в вашем скрипте / приложении, затем продолжайте удаление в пакетах (скажем, 50 000 строк за раз) , Это позволит достичь следующего:

  • каждый из операторов удаления не будет слишком долго блокировать таблицу, что не позволит лагу репликации выйти из-под контроля . Это особенно важно, если вы полагаетесь на свою репликацию для предоставления вам относительно актуальных данных. Преимущество использования пакетов состоит в том, что если вы обнаружите, что каждый запрос DELETE по-прежнему занимает слишком много времени, вы можете изменить его на меньший, не затрагивая структуры БД.
  • еще одним преимуществом использования отдельного SELECT является то, что самому SELECT может потребоваться много времени для запуска , особенно если он по какой-либо причине не может использовать лучшие индексы БД. Если SELECT является внутренним по отношению к DELETE, когда весь оператор мигрирует к подчиненным, он должен будет снова выполнить SELECT, потенциально отстая от подчиненных, потому что он должен снова выполнить длинный выбор заново. Отставание от рабов, опять же, сильно страдает. Если вы используете отдельный запрос SELECT, эта проблема исчезнет, ​​так как все, что вы передаете, это список идентификаторов.

Дайте мне знать, если где-то есть ошибка в моей логике.

Дополнительную информацию о задержке репликации и способах борьбы с ней, аналогичной этой, см. В Объяснение задержки MySQL Slave (задержка) и 7 способов борьбы с ней

P.S. Одна вещь, о которой следует быть осторожным, это, конечно, потенциальные изменения в таблице между моментами окончания SELECT и DELETE. Я позволю вам обрабатывать такие детали с помощью транзакций и / или логики, относящейся к вашему приложению.

3 голосов
/ 01 мая 2009

Попробуйте это:

DELETE QUICK A.* FROM A,B WHERE A.ID=B.ID

Это намного быстрее, чем обычные запросы.

См. Синтаксис: http://dev.mysql.com/doc/refman/5.0/en/delete.html

3 голосов
/ 01 мая 2009

Вы выполняете свой подзапрос для 'b' для каждой строки в 'a'.

Попытка:

DELETE FROM a USING a LEFT JOIN b ON a.id = b.id WHERE b.id IS NOT NULL;
2 голосов
/ 12 мая 2009

Кстати, после публикации вышеизложенного в моем блоге, Барон Шварц из Percona обратил мое внимание на то, что его maatkit уже имеет инструмент для этой цели - mk-archiver. http://www.maatkit.org/doc/mk-archiver.html.

Скорее всего, это ваш лучший инструмент для работы.

2 голосов
/ 06 мая 2009

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

Другим подходом будет добавление столбца флага deleted в таблицу и настройка других запросов, чтобы они учитывали это значение. Самый быстрый логический тип в mysql - CHAR(0) NULL (true = '', false = NULL). Это будет быстрая операция, после чего вы можете удалить значения.

Те же мысли, высказанные в выражениях sql:

ALTER TABLE a ADD COLUMN deleted CHAR(0) NULL DEFAULT NULL;

-- The following query should be faster than the delete statement:
UPDATE a INNER JOIN b SET a.deleted = '';

-- This is the catch, you need to alter the rest
-- of your queries to take the new column into account:
SELECT * FROM a WHERE deleted IS NULL;

-- You can then issue the following queries in a cronjob
-- to clean up the tables:
DELETE FROM a WHERE deleted IS NOT NULL;

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

2 голосов
/ 06 мая 2009

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

REPAIR TABLE a QUICK;
REPAIR TABLE b QUICK;

и затем выполните любой из указанных выше запросов (т. Е.)

DELETE FROM a WHERE id IN (SELECT id FROM b)
...