모든 열, 심지어 변경되지 않은 열을 업데이트하는 오버 헤드는 무엇입니까?


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속성 에 대한 인덱스가 있다고 가정 하면 DB가 값이 변경되지 않았다는 것을 인식해서는 안됩니까?

에서 이 글 , 마르쿠스 Winand는 말합니다 :

모든 열에 대한 업데이트는 이전 섹션에서 이미 관찰 한 것과 동일한 패턴을 보여줍니다. 응답 시간은 추가 인덱스마다 증가합니다.

데이터베이스가 디스크에서 관련 데이터 페이지를 메모리로로드하여 열 값을 변경해야하는지 여부를 알아낼 수 있기 때문에 왜이 오버 헤드가 발생하는지 궁금합니다.

인덱스의 경우에도 변경되지 않은 열에 대해서는 인덱스 값이 변경되지 않지만 UPDATE에 포함 되었기 때문에 아무것도 재조정하지 않습니다.

데이터베이스에서 리프 값이 여전히 동일하다는 것을 인식하기 위해 변경되지 않은 중복 열과 연관된 B + 트리 인덱스도 탐색해야합니까?

물론 일부 ORM 도구를 사용하면 변경된 속성 만 업데이트 할 수 있습니다.

UPDATE post
SET    score = 12,
WHERE  id = 1

그러나이 유형의 UPDATE는 행마다 다른 특성이 변경 될 때 배치 업데이트 또는 명령문 캐싱의 이점을 항상 얻지 못할 수 있습니다.


1
데이터베이스는 PostgreSQL을했다 (또는 다른 사람을 사용하는 경우 MVCC ),는 UPDATEA와 실질적으로 동일하다 DELETE+ INSERT(당신이 실제로 새로운 만들 수 있기 때문에 V의 행의 ersion을). 오버 헤드는 높고 인덱스 수에 따라 증가 합니다 . 특히이를 구성하는 많은 열이 실제로 업데이트 되고 인덱스를 나타내는 데 사용 된 트리 (또는 기타)가 크게 변경되어야하는 경우에 특히 그렇습니다. 관련 항목이 업데이트되는 열 수는 아니지만 인덱스의 열 부분을 업데이트하는지 여부입니다.
joanolo

@joanolo 이것은 postgres의 MVCC 구현에만 해당됩니다. MySQL, Oracle (및 기타)은 업데이트를 수행하고 변경된 열을 UNDO 공간으로 재배치합니다.
Morgan Tocker 2016 년

2
좋은 ORM은 어떤 열을 업데이트해야하는지 추적하고 데이터베이스로 전송 된 명령문을 최적화해야합니다. 특히 일부 열이 긴 텍스트 또는 BLOB 인 경우 DB에 전송되는 데이터의 양에 대해서만 관련이 있습니다 .
joanolo


2
어떤 DBMS를 사용하고 있습니까?
a_horse_with_no_name

답변:


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;

JDBC를 포함한 대부분의 데이터베이스 클라이언트 API 및 결과적으로 JPA는 바인딩을 허용하지 않습니다. DEFAULT 표현식이 바인드 변수 대부분 서버가이를 허용하지 않기 때문입니다. 위에서 언급 한 배치 가능성 및 명령문 캐시 가능성 이유로 동일한 SQL 문을 재사용하려면 두 경우 모두 다음 명령문을 사용해야합니다 ( (a, b, c)모두 열이 모두 있다고 가정 t).

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

그리고 이후 c설정되지 않은, 당신은 아마 바인드 자바 거라고 null많은으로 ORMs도 구별 할 수 없기 때문에, 세 번째 바인드 변수 NULLDEFAULT( 예를 들어 예외 인 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이 "changed" , "null""default" 의 차이를 추적 할 수있는 경우).


DEFAULT사용 케이스에 의해 해결 될 수있다 @DynamicInsert. TRIGGER 상황은 다음과 같은 확인을 사용 WHEN (NEW.b <> OLD.b)하거나로 전환 하여 해결할 수도 있습니다 @DynamicUpdate.
Vlad Mihalcea 2016 년

그렇습니다. 문제를 해결할 수는 있지만 원래 성능을 요구 한 경우 해결 방법에 더 많은 오버 헤드가 추가되었습니다.
Lukas Eder

모건이 가장 잘했다고 생각 합니다. 복잡 합니다.
Vlad Mihalcea 7

나는 그것이 간단하다고 생각합니다. 프레임 워크 관점에서 동적 SQL을 기본값으로 사용하기 위해 더 많은 인수가 있습니다. 사용자 관점에서 볼 때 복잡합니다.
Lukas Eder

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-committedsession1 커밋 전에 session2 읽기가 발생하는 격리에 대한 시간 창이 있습니다 .

다시 말하면 : SELECT .. FOR UPDATE를 사용하여 행을 읽은 다음에 a 를 발행하는 경우에만 최적화가 안전 합니다 UPDATE. SELECT .. FOR UPDATEMVCC를 사용하지 않고 항상 최신 버전의 행을 읽습니다.


편집 : 테스트 케이스 데이터 세트가 메모리에서 100 %인지 확인하십시오. 조정 된 타이밍 결과.


설명 주셔서 감사합니다. 저도 직감입니다. DB가 데이터 페이지의 행과 모든 관련 인덱스를 모두 확인한다고 생각합니다. 열이 매우 크거나 관련된 인덱스가 많으면 오버 헤드가 눈에 띄게 될 수 있습니다. 그러나 대부분의 상황에서 소형 열 유형을 사용하고 필요한만큼의 인덱스를 사용하는 경우 오버 헤드가 명령문 캐싱의 이점을 얻지 못하거나 명령문을 일괄 처리 할 가능성이 적을 수 있습니다.
Vlad Mihalcea

1
@VladMihalcea는 MySQL에 대한 답변입니다. 결론은 다른 DBMS에서 동일하지 않을 수 있습니다.
ypercubeᵀᴹ

@ ypercube 알고 있습니다. 그것은 모두 RDBMS에 달려 있습니다.
Vlad Mihalcea
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.