Hibernate batch-delete vs single delete

EDIT: на основании некоторых моих отладок и протоколирования я думаю, что вопрос сводится к тому, что DELETE FROM table WHERE id = x намного быстрее, чем DELETE FROM table WHERE id IN (x), где x - всего лишь один идентификатор.

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

Я включил некоторую информацию ниже, пожалуйста, дайте мне знать, если я упустил что-то важное.

Удаление производится партиями по 10 000, а код выглядит примерно так:

private void batchDeletion( Collection<Long> ids ) {
  StringBuilder sb = new StringBuilder();
  sb.append( "DELETE FROM ObjImpl WHERE id IN (:ids)" );

  Query sql = getSession().createQuery( sb.toString() );
  sql.setParameterList( "ids", ids );

  sql.executeUpdate();
}

Код для удаления только одной строки в основном:

SessionFactory.getCurrentSession().delete(obj);

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

Вот пример ANPLYIN EXPLAIN DELETE FROM table where id IN ( 1, 2, 3 );:

Delete on table  (cost=12.82..24.68 rows=3 width=6) (actual time=0.143..0.143 rows=0 loops=1)
  ->  Bitmap Heap Scan on table  (cost=12.82..24.68 rows=3 width=6) (actual time=0.138..0.138 rows=0 loops=1)
        Recheck Cond: (id = ANY ('{1,2,3}'::bigint[]))
        ->  Bitmap Index Scan on pk_table  (cost=0.00..12.82 rows=3 width=0) (actual time=0.114..0.114 rows=0 loops=1)
              Index Cond: (id = ANY ('{1,2,3}'::bigint[]))
Total runtime: 3.926 ms

Я пылесосил и переиндексировал каждый раз, когда я перезагружал свои данные для тестирования, и мои тестовые данные содержат 386 660 строк.

Тест состоит в том, чтобы удалить все строки, и я не использую TRUNCATE, потому что обычно есть критерии выбора, но для целей тестирования я сделал критерии, включающие все строки. С включенными триггерами удаление каждой строки по одному занимает 193,616 мс, тогда как пакетное удаление занимает 285 558 мс. Затем я отключил триггеры и получил 93 793 мс для удаления одной строки и 181 537 мс для пакетного удаления. Триггер идет и суммирует значения и обновляет другую таблицу - в основном бухгалтерию.

Я играл с меньшими размерами партии (100 и 1), и все они выглядят хуже.

EDIT: Включено ведение журнала Hibernate и для удаления по одной строке за строкой, в основном это: delete from table where id=? и EXPLAIN ANALYZE:

Delete on table  (cost=0.00..8.31 rows=1 width=6) (actual time=0.042..0.042 rows=0 loops=1)
  ->  Index Scan using pk_table on table  (cost=0.00..8.31 rows=1 width=6) (actual time=0.037..0.037 rows=0 loops=1)
        Index Cond: (id = 3874904)
Total runtime: 0.130 ms

РЕДАКТИРОВАТЬ: Было любопытно, действительно ли список содержит 10 000 идентификаторов, если Postgres сделает что-то другое: нет.

Delete on table  (cost=6842.01..138509.15 rows=9872 width=6) (actual time=17.170..17.170 rows=0 loops=1)
  ->  Bitmap Heap Scan on table  (cost=6842.01..138509.15 rows=9872 width=6) (actual time=17.160..17.160 rows=0 loops=1)
        Recheck Cond: (id = ANY ('{NUMBERS 1 THROUGH 10,000}'::bigint[]))
        ->  Bitmap Index Scan on pk_table  (cost=0.00..6839.54 rows=9872 width=0) (actual time=17.139..17.139 rows=0 loops=1)
              Index Cond: (id = ANY ('{NUMBERS 1 THROUGH 10,000}'::bigint[]))
Total runtime: 17.391 ms

EDIT: на основе EXPLAIN ANALYZE из вышеизложенного я получил некоторые записи из фактических операций удаления. Ниже приведена запись двух вариантов удаления одной строки за строкой.

Вот несколько удалений:

2013-03-14 13:09:25,424:delete from table where id=?
2013-03-14 13:09:25,424:delete from table where id=?
2013-03-14 13:09:25,424:delete from table where id=?
2013-03-14 13:09:25,424:delete from table where id=?
2013-03-14 13:09:25,424:delete from table where id=?
2013-03-14 13:09:25,424:delete from table where id=?
2013-03-14 13:09:25,424:delete from table where id=?
2013-03-14 13:09:25,424:delete from table where id=?
2013-03-14 13:09:25,424:delete from table where id=?
2013-03-14 13:09:25,424:delete from table where id=?

Вот другая вариация одиночных удалений (список всего 1 элемент)

2013-03-14 13:49:59,858:delete from table where id in (?)
2013-03-14 13:50:01,460:delete from table where id in (?)
2013-03-14 13:50:03,040:delete from table where id in (?)
2013-03-14 13:50:04,544:delete from table where id in (?)
2013-03-14 13:50:06,125:delete from table where id in (?)
2013-03-14 13:50:07,707:delete from table where id in (?)
2013-03-14 13:50:09,275:delete from table where id in (?)
2013-03-14 13:50:10,833:delete from table where id in (?)
2013-03-14 13:50:12,369:delete from table where id in (?)
2013-03-14 13:50:13,873:delete from table where id in (?)

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


ОБЪЯСНЕНИЕ АНАЛИЗА DELETE FROM table WHERE id = 3774887;

Delete on table  (cost=0.00..8.31 rows=1 width=6) (actual time=0.097..0.097 rows=0 loops=1)
  ->  Index Scan using pk_table on table  (cost=0.00..8.31 rows=1 width=6) (actual time=0.055..0.058 rows=1 loops=1)
        Index Cond: (id = 3774887)
Total runtime: 0.162 ms

EXPLAIN ANALYZE DELETE FROM table WHERE id IN (3774887);

Delete on table  (cost=0.00..8.31 rows=1 width=6) (actual time=0.279..0.279 rows=0 loops=1)
  ->  Index Scan using pk_table on table  (cost=0.00..8.31 rows=1 width=6) (actual time=0.210..0.213 rows=1 loops=1)
        Index Cond: (id = 3774887)
Total runtime: 0.452 ms

0,162 против 0,452 считали значительную разницу?

EDIT:

Установите размер партии до 50 000, а Hibernate не понравится эта идея:

java.lang.StackOverflowError
        at org.hibernate.hql.ast.util.NodeTraverser.visitDepthFirst(NodeTraverser.java:40)
        at org.hibernate.hql.ast.util.NodeTraverser.visitDepthFirst(NodeTraverser.java:41)
        at org.hibernate.hql.ast.util.NodeTraverser.visitDepthFirst(NodeTraverser.java:42)
....

Ответ 1

Хорошо, первое, что вы должны отметить, это то, что SQL должен каким-то образом преобразоваться в план. Результаты вашего EXPLAIN показывают, что логика здесь принципиально отличается для равенства по сравнению с конструкцией IN (vals).

WHERE id = 1;

Преобразуется в простой фильтр равенства.

WHERE id IN (1);

Преобразуется в соответствие массива:

WHERE id = ANY(ARRAY[1]);

По-видимому, планировщик недостаточно умен, чтобы заметить, что они математически идентичны, когда массив имеет ровно один член. Так что это планирование массива любого размера, поэтому вы получаете сканирование растрового изображения вложенного цикла.

Интересно здесь не только то, что он медленнее, но производительность по большей части превосходит по большей части. Таким образом, с одним членом в предложении in() он работает в 40 раз медленнее, а с 10000 членами он только в 170 раз медленнее, но это также означает, что версия 10000-членов также в 50 раз быстрее, чем 10000 отдельных сканирующих индексов на идентификаторе.

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

Ответ 2

Если проблема здесь действительно сводится к "как я могу удалить множество записей как можно быстрее?" то метод DELETE... IN() будет превосходить удаления для каждой отдельной строки, поэтому преследуя причины, по которым IN (?) с одним членом оказывается медленнее, чем =? не поможет вам.

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

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

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