Яке накладне оновлення всіх стовпців, навіть тих, що не змінилися [закрито]


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 -рядок рядка). Накладні витрати великі і зростають із кількістю індексів , особливо якщо багато стовпців, що їх складають, фактично оновлюються, і дерево (або будь-яке інше), що використовується для представлення індексу, потребує значних змін. Релевантна не кількість стовпців, які оновлюються, але чи оновлення стовпця індексу.
joanolo

@joanolo Це має бути справедливим лише для постгресової реалізації MVCC. MySQL, Oracle (та інші) роблять оновлення на місці та переміщують змінені стовпці до простору UNDO.
Морган Токер

2
Я мушу зазначити, що хороший ORM повинен відстежувати, які стовпці потребують оновлення, та оптимізувати заяву, надіслану до бази даних. Це актуально, якщо тільки для кількості даних, переданих до БД, особливо якщо деякі стовпці - це довгі тексти або BLOB .
joanolo

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 довгим текстом потребує часу (оскільки воно розділено на багато сторінок). Але, здається, сама модифікація не проходить через журнал повторень.

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

Більш довгий відповідь

Я фактично думаю, що ОРМ не повинна усунути стовпці, які були змінені ( але не змінені ), оскільки ця оптимізація має дивні побічні ефекти.

Розглянемо наступне у псевдокоді:

# 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.

Інакше кажучи: оптимізація безпечна лише в тому випадку, якщо ви видалите SELECT .. FOR UPDATEдля читання рядків, за якими йде а UPDATE. SELECT .. FOR UPDATEне використовує MVCC і завжди читає останню версію рядків.


Редагувати: переконайтеся, що набір даних тестового випадку на 100% у пам'яті. Відрегульовані результати часу.


Дякую за пояснення. Це теж моя інтуїція. Думаю, БД перевірить і рядок на сторінці даних, і всі пов'язані з цим індекси. Якщо стовпчик дуже великий або в ньому задіяні тонни індексів, накладні витрати можуть стати помітними. Але в більшості ситуацій, коли використовуються компактні типи стовпців і стільки ж індексів, скільки потрібно, я гадаю, що накладні витрати можуть бути меншими, ніж не мати користі від кешування висловлювань або мати менший шанс отримати пакет оператора.
Влад Міхалча

1
@VladMihalcea будьте обережні, що відповідь стосується MySQL. Висновки можуть бути не однаковими для різних СУБД.
ypercubeᵀᴹ

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