본문 바로가기
빌드노트 검색

빌드노트

PostgreSQL UPDATE에서 b=a가 증가 전 a를 복사하는 이유

PostgreSQL의 SET a=a+1, b=a는 b에 갱신 전 a를 넣는다. 같은 한 행에서 항목 순서 변경, 두 열 교환, b=a+1을 비교하고 RETURNING이 보여 주는 갱신 후 값과 구별한다.

PostgreSQL에서 SET a = a + 1, b = a를 실행하면, a가 10에서 11로 바뀌어도 b에는 10이 들어간다. 같은 SET 안의 b = a가 읽는 것은 그 행의 갱신 전 a이기 때문이다. 두 열에 모두 증가한 값을 넣으려면 b = a + 1도 명시해야 한다.

PostgreSQL 18의 UPDATE 문서 (새 탭에서 열림)는 SET 표현식에서 대상 열과 다른 열의 이전 값을 참조할 수 있다고 설명한다. 오른쪽 식에 쓴 a를 앞 항목이 만든 새 값으로 해석하면 결과를 잘못 예상하게 된다.

같은 행에서 SET과 RETURNING이 읽는 값

아래는 PostgreSQL 18.6에서 직접 실행한 예제다. 별도의 실습 DB에 연결한 새 SQL 세션에서 순서대로 실행한다. 임시 테이블을 만들 수 있는 권한이 필요하며, 마지막 ROLLBACK까지 같은 연결을 유지한다. 출력은 psql의 CSV 표시 (새 탭에서 열림)인 --csv 형식이다. 다른 클라이언트에서는 표 모양이 달라도 a, b의 값을 비교하면 된다.

먼저 (a,b)=(10,20)인 한 행을 만들고, 갱신 전 조회와 RETURNING을 비교한다.

BEGIN;
CREATE TEMP TABLE set_values_lab (
    id integer PRIMARY KEY,
    a integer,
    b integer
);
INSERT INTO set_values_lab VALUES (1, 10, 20);
SELECT a, b FROM set_values_lab WHERE id = 1;

UPDATE set_values_lab
SET a = a + 1, b = a
WHERE id = 1
RETURNING a, b;

첫 SELECT는 10,20을 반환하고, RETURNING은 다음 값을 반환했다.

a,b
11,10

a = a + 1은 이전 a인 10에 1을 더해 11을 만든다. b = a 역시 이전 a인 10을 읽어 b에 넣는다. 두 항목의 오른쪽에 같은 a를 썼지만, 두 번째 항목으로 11이 전달되지는 않는다.

여기서 RETURNING a, b는 갱신된 행의 값이다. 공식 문서도 일반적인 열 이름을 쓴 RETURNING은 갱신 후 값을 반환한다고 명시한다. 따라서 출력의 a=11과 b=10은 서로 다른 조회 시점이 섞인 것이 아니라, 이번 갱신이 만든 한 행의 결과다.

SET 순서를 바꿔도 새 a를 넘겨주지 않는다

같은 초기값으로 돌린 뒤 두 항목의 순서만 바꿔 본다. 앞선 실행 결과를 그대로 쓰면 이미 a가 11이므로 비교 조건이 달라진다.

UPDATE set_values_lab SET a = 10, b = 20 WHERE id = 1;
UPDATE set_values_lab
SET b = a, a = a + 1
WHERE id = 1
RETURNING a, b;

이번에도 결과는 a=11, b=10이었다. b = a를 뒤에 적어도, 앞에 적어도 읽는 값은 10이다. 항목을 재배치하는 것으로는 b에 증가한 값을 복사할 수 없다.

두 열을 맞바꾸면 이 차이가 더 잘 보인다. 다시 (10,20)에서 시작해, 각 열에 다른 열의 이전 값을 넣는다.

UPDATE set_values_lab SET a = 10, b = 20 WHERE id = 1;
UPDATE set_values_lab
SET a = b, b = a
WHERE id = 1
RETURNING a, b;

실제 결과는 다음과 같았다.

a,b
20,10

a에는 이전 b인 20이, b에는 이전 a인 10이 들어갔다. 왼쪽 항목부터 값을 덮어쓰는 대입문으로 읽었다면 두 열 모두 20이 된다고 예상했을 것이다. 이 교환은 한 행의 두 정수 열에 관한 예제이며, 서로 다른 행의 UNIQUE 값을 교환할 때의 제약 검사와는 별개다.

두 열에 11을 넣으려면 원하는 식을 각각 쓴다

목표가 “이전 a보다 1 큰 값을 두 열에 넣기”라면 두 오른쪽 식을 모두 a + 1로 쓴다. 다음 입력은 초기값 복원, 수정한 갱신, 실습 정리를 차례로 수행한다.

UPDATE set_values_lab SET a = 10, b = 20 WHERE id = 1;
UPDATE set_values_lab
SET a = a + 1, b = a + 1
WHERE id = 1
RETURNING a, b;
ROLLBACK;

이번 RETURNING 결과는 두 열 모두 11이었다. 뒤의 ROLLBACK으로 실습에서 만든 테이블과 변경을 되돌린다.

a,b
11,11

기존 쿼리를 고칠 때도 먼저 b에 남기려는 값이 이전 a인지, 증가한 a인지 정하면 된다. 이전 값을 보관하려는 목적이면 b = a가 맞다. 증가한 값을 함께 저장하려는 목적이면 이 예제처럼 b = a + 1로 계산을 적는다. a + 1은 같은 정수 입력에 같은 결과를 내는 계산이며, 상태를 바꾸는 함수 호출을 두 번 적었을 때의 동작까지 이 예제로 판단하지는 않는다.

확인한 범위는 트리거 없는 한 행에서의 정수 계산과 RETURNING이다. 같은 입력으로 다른 값이 나온다면 SELECT version();으로 DB 종류와 버전을 확인하고, 실제로 보낸 SET 식과 대상 행을 다시 대조한다. 트리거가 값을 바꾸는 경우, 동시 갱신, 다른 DBMS의 대입 규칙은 이번 실험에서 검증하지 않았다.

← 전체 글 보기