Каковы затраты на обновление всех столбцов, даже тех, которые не изменились [закрыто]

17

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

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

Итак, если я загружаю сущность и устанавливаю только одно свойство:

Post post = entityManager.find(Post.class, 1L);
post.setScore(12);

Все столбцы будут изменены:

UPDATE post
SET    score = 12,
       title = 'High-Performance Java Persistence'
WHERE  id = 1

Теперь, если предположить, что у нас есть индекс и для titleсвойства, не должна ли БД понять, что значение все равно не изменилось?

В этой статье Маркус Винанд говорит:

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

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

Даже для индексов это ничего не меняет, поскольку значения индексов для столбцов, которые не изменились, не изменились, но они были включены в ОБНОВЛЕНИЕ.

Нужно ли перемещаться по индексам B + Tree, связанным с избыточными неизмененными столбцами, только для того, чтобы база данных поняла, что значение листа остается прежним?

Конечно, некоторые инструменты ORM позволяют ОБНОВЛЯТЬ только измененные свойства:

UPDATE post
SET    score = 12,
WHERE  id = 1

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

Влад Михалча
источник
1
Если база данных была PostgreSQL (или некоторые другие , что использование MVCC ), UPDATEпрактически эквивалентно DELETE+ INSERT(потому что вы на самом деле создать новый V ersion в ряду). Издержки высоки и растут с увеличением количества индексов , особенно если многие столбцы, которые их содержат, фактически обновлены, а дерево (или что-то еще), используемое для представления индекса, нуждается в значительном изменении. Важно не количество столбцов, которые обновляются, а то, обновляете ли вы часть столбца индекса.
Жуаноло
@joanolo Это нужно только для реализации MVCC в postgres. MySQL, Oracle (и другие) выполняют обновление на месте и перемещают измененные столбцы в пространство UNDO.
Морган Токер
2
Я должен отметить, что хороший ORM должен отслеживать, какие столбцы нужно обновить, и оптимизировать оператор, отправляемый в базу данных. Это актуально, если только для объема данных, передаваемых в БД, особенно если некоторые столбцы представляют собой длинные тексты или большие двоичные объекты .
Жуаноло
1
Вопрос, обсуждающий это для SQL Server dba.stackexchange.com/q/114360/3690
Мартин Смит
2
Какую СУБД вы используете?
a_horse_with_no_name

Ответы:

12

Я знаю, что вы в основном озабочены UPDATEи в основном производительностью, но как сотрудник службы поддержки «ORM», позвольте мне дать вам еще один взгляд на проблему различия между «измененными» , «нулевыми» и «значениями по умолчанию» , которые три разные вещи в SQL, но, возможно, только одна вещь в Java и в большинстве ORM:

Перевод вашего обоснования в INSERTзаявления

Ваши аргументы в пользу пакетности и кэширования операторов верны для INSERTоператоров так же, как и для UPDATEоператоров. Но в случае INSERTоператоров, исключение столбца из оператора имеет другую семантику, чем в UPDATE. Это значит применять DEFAULT. Следующие два семантически эквивалентны:

INSERT INTO t (a, b)    VALUES (1, 2);
INSERT INTO t (a, b, c) VALUES (1, 2, DEFAULT);

Это не так, поскольку UPDATEпервые два семантически эквивалентны, а третий имеет совершенно другое значение:

-- These are the same
UPDATE t SET a = 1, b = 2;
UPDATE t SET a = 1, b = 2, c = c;

-- This is different!
UPDATE t SET a = 1, b = 2, c = DEFAULT;

Большинство клиентских API баз данных, в том числе JDBC и, как следствие, JPA, не позволяют связывать DEFAULTвыражение с переменной связывания - в основном потому, что серверы также не допускают этого. Если вы хотите повторно использовать один и тот же оператор SQL по вышеупомянутым причинам пакетности и кеширования операторов, вы должны использовать следующий оператор в обоих случаях (при условии (a, b, c), что все столбцы в t):

INSERT INTO t (a, b, c) VALUES (?, ?, ?);

И поскольку cон не установлен, вы, вероятно, связали бы Java nullс третьей переменной связывания, потому что многие ORM также не могут различать между NULLи DEFAULT( jOOQ , например, здесь исключение). Они видят только Java nullи не знают, означает ли это NULL(как в неизвестном значении) или DEFAULT(как в неинициализированном значении).

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

  • Есть DEFAULTпункт
  • Это может быть сгенерировано триггером

Вернуться к UPDATEзаявлениям

Хотя вышеприведенное верно для всех баз данных, я могу заверить вас, что проблема с триггером верна и для базы данных Oracle. Рассмотрим следующий SQL:

CREATE TABLE x (a INT PRIMARY KEY, b INT, c INT, d INT);

INSERT INTO x VALUES (1, 1, 1, 1);

CREATE OR REPLACE TRIGGER t
  BEFORE UPDATE OF c, d
  ON x
BEGIN
  IF updating('c') THEN
    dbms_output.put_line('Updating c');
  END IF;
  IF updating('d') THEN
    dbms_output.put_line('Updating d');
  END IF;
END;
/

SET SERVEROUTPUT ON
UPDATE x SET b = 1 WHERE a = 1;
UPDATE x SET c = 1 WHERE a = 1;
UPDATE x SET d = 1 WHERE a = 1;
UPDATE x SET b = 1, c = 1, d = 1 WHERE a = 1;

Когда вы запустите выше, вы увидите следующий вывод:

table X created.
1 rows inserted.
TRIGGER T compiled
1 rows updated.
1 rows updated.
Updating c

1 rows updated.
Updating d

1 rows updated.
Updating c
Updating d

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

Другими словами:

Текущее поведение Hibernate, которое вы описываете, является неполным и может даже рассматриваться как неправильное в присутствии триггеров (и, возможно, других инструментов).

Я лично думаю, что ваш аргумент оптимизации кэша запросов переоценен в случае динамического SQL. Конечно, в таком кеше будет еще несколько запросов и немного больше работы по анализу, но обычно это не проблема для динамических UPDATEоператоров, гораздо меньше, чем для SELECT.

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

Лукас Эдер
источник
Вариант DEFAULTиспользования может быть адресован @DynamicInsert. Ситуация TRIGGER также может быть решена с помощью проверок типа WHEN (NEW.b <> OLD.b)или просто переключиться на @DynamicUpdate.
Влад Михалча
Да, все можно решить, но вы изначально спрашивали о производительности, и ваш обходной путь добавляет еще больше накладных расходов.
Лукас Эдер
Я думаю, что Морган сказал это лучше всего: это сложно .
Влад Михалча
Я думаю, что это довольно просто. С точки зрения платформы, есть больше аргументов в пользу дефолта к динамическому SQL. С точки зрения пользователя, да, это сложно.
Лукас Эдер
9

Я думаю, что ответ - это сложно . Я попытался написать быстрое доказательство, используя longtextколонку в MySQL, но ответ немного неубедителен. Доказательство первое:

# in advance:
set global max_allowed_packet=1024*1024*1024;

CREATE TABLE `t2` (
  `a` int(11) NOT NULL AUTO_INCREMENT,
  `b` char(255) NOT NULL,
  `c` LONGTEXT,
  PRIMARY KEY (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

mysql> insert into t2 (a, b, c) values (null, 'b', REPEAT('c', 1024*1024*1024));
Query OK, 1 row affected (38.81 sec)

mysql> UPDATE t2 SET b='new'; # fast
Query OK, 1 row affected (6.73 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql>  UPDATE t2 SET b='new'; # fast
Query OK, 0 rows affected (2.87 sec)
Rows matched: 1  Changed: 0  Warnings: 0

mysql> UPDATE t2 SET b='new'; # fast
Query OK, 0 rows affected (2.61 sec)
Rows matched: 1  Changed: 0  Warnings: 0

mysql> UPDATE t2 SET c= REPEAT('d', 1024*1024*1024); # slow (changed value)
Query OK, 1 row affected (22.38 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> UPDATE t2 SET c= REPEAT('d', 1024*1024*1024); # still slow (no change)
Query OK, 0 rows affected (14.06 sec)
Rows matched: 1  Changed: 0  Warnings: 0

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

mysql> show global status like 'innodb_pages_written';
+----------------------+--------+
| Variable_name        | Value  |
+----------------------+--------+
| Innodb_pages_written | 198656 |
+----------------------+--------+
1 row in set (0.00 sec)

mysql> show global status like 'innodb_pages_written';
+----------------------+--------+
| Variable_name        | Value  |
+----------------------+--------+
| Innodb_pages_written | 198775 | <-- 119 pages changed in a "no change"
+----------------------+--------+
1 row in set (0.01 sec)

mysql> show global status like 'innodb_pages_written';
+----------------------+--------+
| Variable_name        | Value  |
+----------------------+--------+
| Innodb_pages_written | 322494 | <-- 123719 pages changed in a "change"!
+----------------------+--------+
1 row in set (0.00 sec)

Таким образом, похоже, что время увеличилось, потому что должно быть сравнение, чтобы подтвердить, что само значение не было изменено, что в случае длинного текста 1G занимает время (потому что оно разбито на много страниц). Но сама модификация, похоже, не откатывается через журнал повторов.

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

Более длинный ответ

Я на самом деле думаю, что ORM не должен исключать столбцы, которые были изменены ( но не изменены ), так как эта оптимизация имеет странные побочные эффекты.

Рассмотрим следующее в псевдокоде:

# Initial Data does not make sense
# should be either "Harvey Dent" or "Two Face"

id: 1, firstname: "Two Face", lastname: "Dent"

session1.start
session2.start

session1.firstname = "Two"
session1.lastname = "Face"
session1.save

session2.firstname = "Harvey"
session2.lastname = "Dent"
session2.save

Результат, если ORM «оптимизировать» модификацию без изменений:

id: 1, firstname: "Harvey", lastname: "Face"

Результат, если ORM отправил все модификации на сервер:

id: 1, firstname: "Harvey", lastname: "Dent"

Тест-кейс здесь опирается на repeatable-read изоляция (по умолчанию MySQL), но также существует временное окно для read-committedизоляции, когда чтение сессии2 происходит до фиксации сессии1.

Другими словами: оптимизация безопасна только в том случае, если вы запускаете a SELECT .. FOR UPDATEдля чтения строк, за которыми следуетUPDATE . SELECT .. FOR UPDATEне использует MVCC и всегда читает последнюю версию строк.


Редактировать: Убедитесь, что набор данных теста был 100% в памяти. Скорректированы сроки результатов.

Морган Токер
источник
Спасибо за объяснение. Это тоже моя интуиция. Я думаю, что БД проверит как строку на странице данных, так и все связанные индексы. Если столбец очень большой или в нем задействовано множество индексов, накладные расходы могут стать заметными. Но для большинства ситуаций, когда используются компактные типы столбцов и столько индексов, сколько необходимо, я предполагаю, что издержки могут быть меньше, чем отсутствие выгоды от кэширования операторов или меньшего шанса пакетирования операторов.
Влад Михальча
1
@VladMihalcea остерегайтесь, что ответ о MySQL. Выводы могут не совпадать в разных СУБД.
ypercubeᵀᴹ
@ypercube Я знаю об этом. Все зависит от РСУБД.
Влад Михальча