更新所有列(即使是未更改的列)的开销是多少?


17

在更新行时,许多ORM工具都会发出UPDATE语句来设置与该特定实体相关联的每一列

优点是您可以轻松批处理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属性上也有一个索引,DB难道不应该意识到该值没有改变吗?

本文中,Markus Winand说:

所有列上的更新都显示了我们在上一节中已经观察到的相同模式:响应时间随每个其他索引的增加而增加。

我不知道为什么会产生这种开销,因为数据库将相关的数据页从磁盘加载到内存中,从而可以确定是否需要更改列值。

即使对于索引,它也不会重新平衡任何内容,因为对于未更改的列,索引值不会更改,但是它们已包含在UPDATE中。

是否也需要导航与冗余未更改列关联的B +树索引,只是为了让数据库意识到叶值仍然相同?

当然,某些ORM工具允许您仅更新已更改的属性:

UPDATE post
SET    score = 12,
WHERE  id = 1

但是,当为不同的行更改不同的属性时,这种类型的UPDATE可能并不总是从批量更新或语句缓存中受益。


1
如果数据库是PostgreSQL的(或者其他人在使用MVCC),一个UPDATE是几乎等同于一个DELETE+ INSERT(因为你实际上是创建一个新的V行的版为)。开销很高,并且随着索引数量的增加而增加,特别是如果组成它们的许多列实际上已更新,并且 用于表示索引的(或其他任何东西)需要进行重大更改时,尤其如此。重要的不是要更新的列数,而是是否更新索引的列部分。
joanolo

@joanolo对于Postgres的MVCC实现,这仅是正确的。MySQL,Oracle(及其他)进行了适当的更新,并将更改的列重新定位到UNDO空间。
Morgan Tocker

2
我应该指出,一个好的ORM应该跟踪哪些列需要更新,并优化发送到数据库的语句。如果仅对于传输到DB的数据量而言,这是相关的,尤其是当某些列是长文本BLOB时
joanolo

1
有关为SQL Server讨论此问题的问题dba.stackexchange.com/q/114360/3690
Martin Smith,

2
您正在使用哪个DBMS?
a_horse_with_no_name

Answers:


12

我知道您最关心UPDATE并且主要关注性能,但是作为“ ORM”维护者,让我对区分“ changed”“ null”“ default”值的问题有另一种看法。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未设置,您可能null会将Java绑定到第三个绑定变量,因为许多ORM也无法区分NULLDEFAULTjOOQ,例如在这里是一个例外)。他们只看到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语句来说,这通常不是问题,比for少得多SELECT

批处理当然是一个问题,但是我认为,不应单单更新一次以更新所有列,因为仅仅存在语句可批处理的可能性。可能的是,ORM可以收集连续相同语句的子批,然后对那些子批进行批处理,而不是“整个批”(如果ORM甚至能够跟踪“ changed”“ null”“ default”之间的差异)


DEFAULT用例可以得到解决@DynamicInsert。也可以使用诸如之类的检查WHEN (NEW.b <> OLD.b)或仅切换到来解决TRIGGER的情况@DynamicUpdate
Vlad Mihalcea

是的,可以解决问题,但是您最初是在询问性能,而解决方法却增加了更多开销。
卢卡斯·埃德

我认为摩根说的最好:很复杂
Vlad Mihalcea

我认为这很简单。从框架的角度来看,有更多的参数支持默认为动态SQL。从用户的角度来看,是的,这很复杂。
卢卡斯·埃德

9

我认为答案是- 很复杂。我试图用longtextMySQL中的一栏写一个快速的证明,但是答案是不确定的。首先证明:

# 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隔离时间窗口,其中在session1提交之前进行session2读取。

换句话说:优化仅在发出a SELECT .. FOR UPDATE来读取后跟一个行的情况下才是安全的UPDATESELECT .. FOR UPDATE不使用MVCC,并且始终读取行的最新版本。


编辑:确保测试用例数据集在内存中为100%。调整了计时结果。


感谢您的解释。这也是我的直觉。我认为数据库将同时检查数据页中的行和所有关联的索引。如果该列非常大或涉及大量索引,则开销可能变得很明显。但是对于大多数情况,当使用紧凑的列类型和所需的尽可能多的索引时,我猜想开销可能比不受益于语句缓存或批处理语句的机会少。
Vlad Mihalcea

1
@VladMihalcea注意答案是关于MySQL的。在不同的DBMS中,结论可能并不相同。
ypercubeᵀᴹ

@ypercube我知道这一点。这一切都取决于RDBMS。
Vlad Mihalcea
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.