Vla*_*cea 19 performance index orm update
在更新一行时,许多 ORM 工具发出一个 UPDATE 语句,设置与该特定实体关联的每一列。
优点是您可以轻松地批量更新语句,因为UPDATE
无论您更改什么实体属性,语句都是相同的。此外,您甚至还可以使用服务器端和客户端语句缓存。
所以,如果我加载一个实体并且只设置一个属性:
Post post = entityManager.find(Post.class, 1L);
post.setScore(12);
Run Code Online (Sandbox Code Playgroud)
所有列都将被更改:
UPDATE post
SET score = 12,
title = 'High-Performance Java Persistence'
WHERE id = 1
Run Code Online (Sandbox Code Playgroud)
现在,假设我们也有一个关于该title
属性的索引,数据库难道不应该意识到该值无论如何都没有改变吗?
在这篇文章中,Markus Winand 说:
所有列的更新显示了我们在前几节中已经观察到的相同模式:响应时间随着索引的增加而增加。
我想知道为什么会有这种开销,因为数据库将关联的数据页从磁盘加载到内存中,因此它可以确定是否需要更改列值。
即使对于索引,它也不会重新平衡任何内容,因为对于未更改的列,索引值不会更改,但它们已包含在 UPDATE 中。
是不是和冗余不变列关联的B+树索引也需要导航,数据库才意识到叶子值还是一样的?
当然,一些 ORM 工具允许您只更新更改的属性:
UPDATE post
SET score = 12,
WHERE id = 1
Run Code Online (Sandbox Code Playgroud)
但是,当不同行的不同属性更改时,这种类型的 UPDATE 可能并不总是从批量更新或语句缓存中受益。
Luk*_*der 13
我知道您最关心UPDATE
和最关心的是性能,但作为“ORM”维护者,让我给您另一个角度来区分“已更改”、“空”和“默认”值的问题,它们是SQL 中的三种不同的东西,但在 Java 和大多数 ORM 中可能只有一种:
INSERT
陈述您支持可批处理性和语句可缓存性的论点对于INSERT
语句和语句一样适用UPDATE
。但是在INSERT
语句的情况下,从语句中省略一列的语义与 in 不同UPDATE
。意思是申请DEFAULT
。以下两个在语义上是等效的:
INSERT INTO t (a, b) VALUES (1, 2);
INSERT INTO t (a, b, c) VALUES (1, 2, DEFAULT);
Run Code Online (Sandbox Code Playgroud)
对于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;
Run Code Online (Sandbox Code Playgroud)
大多数数据库客户端 API,包括 JDBC,因此,JPA,不允许将DEFAULT
表达式绑定到绑定变量 - 主要是因为服务器也不允许这样做。如果出于上述可批处理性和语句可缓存性原因要重新使用相同的 SQL 语句,则在两种情况下(a, b, c)
都应使用以下语句(假设中的所有列都是t
):
INSERT INTO t (a, b, c) VALUES (?, ?, ?);
Run Code Online (Sandbox Code Playgroud)
由于c
未设置,您可能null
会将Java 绑定到第三个绑定变量,因为许多 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;
Run Code Online (Sandbox Code Playgroud)
当你运行上面的程序时,你会看到以下输出:
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
Run Code Online (Sandbox Code Playgroud)
如您所见,始终更新所有列的语句将始终触发所有列的触发器,而仅更新已更改列的语句将仅触发监听此类特定更改的触发器。
您所描述的 Hibernate 当前行为是不完整的,在存在触发器(可能还有其他工具)的情况下甚至可能被认为是错误的。
我个人认为在动态 SQL 的情况下,您的查询缓存优化参数被高估了。当然,这样的缓存中会有更多的查询,需要做更多的解析工作,但这对于动态UPDATE
语句来说通常不是问题,远少于SELECT
.
批处理当然是一个问题,但在我看来,不应该规范化单个更新来更新所有列,因为语句有可能是批处理的。有可能,ORM 可以收集连续相同语句的子批次,并将这些子批次而不是“整个批次”进行批处理(以防 ORM 甚至能够跟踪"changed"、"null"和"default"之间的差异
我认为答案是——这很复杂。我尝试使用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
Run Code Online (Sandbox Code Playgroud)
所以慢+变化值和慢+无变化值之间有一个小的时间差。因此,我决定查看另一个指标,即写入的页面数:
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)
Run Code Online (Sandbox Code Playgroud)
所以看起来时间增加了,因为必须进行比较以确认值本身没有被修改,这在 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
Run Code Online (Sandbox Code Playgroud)
如果 ORM 要“优化”修改而不更改,则结果:
id: 1, firstname: "Harvey", lastname: "Face"
Run Code Online (Sandbox Code Playgroud)
如果 ORM 将所有修改发送到服务器,则结果:
id: 1, firstname: "Harvey", lastname: "Dent"
Run Code Online (Sandbox Code Playgroud)
这里的测试用例依赖于repeatable-read
隔离(MySQL 默认),但也存在用于read-committed
隔离的时间窗口,其中 session2 读取发生在 session1 提交之前。
换句话说:只有当您发出 aSELECT .. FOR UPDATE
以读取后跟一个 的行时,优化才是安全的UPDATE
。 SELECT .. FOR UPDATE
不使用 MVCC 并始终读取行的最新版本。
编辑:确保测试用例数据集在内存中是 100%。调整计时结果。