가격을 일괄로 바꾸는 작업이었다. 미리 뽑아 본 것은 300건이었는데 실제로는 1,200건이 바뀌었다.
Table of contents
Open Table of contents
원인 — 조건이 다른 것을 못 봤다
미리 본 것은 이랬다.
SELECT COUNT(*) FROM product
WHERE category_no = 12 AND use_yn = 'Y';
300
300건이라고 보고했다. 실제로 돌린 것은 이랬다.
UPDATE product SET price = price * 1.1
WHERE category_no = 12;
use_yn = 'Y' 가 빠져 있어서 안 쓰는 상품까지 바뀌었다.
SELECT ROW_COUNT();
1200
SELECT 와 UPDATE 를 따로 썼기 때문이다. 확인용을 먼저 쓰고 나중에 실행용을 새로 쓰면서 use_yn 조건을 빠뜨렸다.
같은 조건이어야 하는 것을 두 번 쓰면 달라진다. 눈으로 훑으면 비슷해 보여서 다르다는 것을 알아채지 못한다.
조건을 한 곳에만 두었다
변수로 두는 것부터 해 봤다.
-- 조건을 변수로 두고
SET @cate = 12;
-- 확인
SELECT COUNT(*) FROM product WHERE category_no = @cate AND use_yn = 'Y';
-- 실행
UPDATE product SET price = price * 1.1 WHERE category_no = @cate AND use_yn = 'Y';
@cate 를 써도 WHERE 뒤는 두 번 쓴다. 그래서 PHP 스크립트로 만들었다.
$where = "category_no = ? AND use_yn = 'Y'";
$params = [12];
// 확인
$rows = $db->query("SELECT no, name, price FROM product WHERE $where", $params);
echo count($rows) . "건이 바뀝니다.\n";
foreach (array_slice($rows, 0, 10) as $r) {
printf(" %d %s %d -> %d\n", $r['no'], $r['name'], $r['price'], (int)($r['price']*1.1));
}
// 확인 뒤 실행
if (readline("진행합니까? (yes) ") !== 'yes') exit;
$db->query("UPDATE product SET price = price * 1.1 WHERE $where", $params);
echo $db->affectedRows() . "건 바뀌었습니다.\n";
$where 가 한 곳에 있으니 확인한 것과 도는 것이 같다.
확인에서 센 것과 실행 뒤 건수도 비교했다.
if ($db->affectedRows() !== count($rows)) {
echo "예상 " . count($rows) . "건인데 " . $db->affectedRows() . "건 바뀌었습니다.\n";
}
affectedRows 가 예상과 다르면 그 사이에 자료가 바뀐 것이다. 조건을 하나로 만들어도 대조는 따로 하는 것이 안전했다.
사본을 먼저 만들었다
바꾸기 전에 되돌릴 것을 만들어 뒀다.
CREATE TABLE product_price_backup_20140819 AS
SELECT no, price FROM product WHERE category_no = 12 AND use_yn = 'Y';
product_price_backup_20140819 에 같은 조건으로 뽑아 둔다.
UPDATE product p JOIN product_price_backup_20140819 b ON p.no = b.no
SET p.price = b.price;
1,200건이 바뀐 것을 이걸로 되돌렸는데 백업은 300건뿐이었다. 그 300건은 돌아갔고 나머지 900건은 따로 찾아서 고쳤다.
백업 범위도 조건을 따라간다. 조건이 틀리면 백업도 틀린다는 것을 그때 알았다.
트랜잭션으로 묶을 수 있는지도 봤다.
$db->beginTransaction();
$rows = $db->query("SELECT ... FOR UPDATE");
// 확인 출력
// 사람이 판단
$db->query("UPDATE ...");
$db->commit();
FOR UPDATE 로 잠그면 그 사이 안 바뀐다. 그런데 사람이 판단하는 동안 잠겨 있으면 서비스가 멈춘다. 건수가 적으면 BEGIN 으로 묶고 많으면 잠그지 않고 건수 비교로 했다.
실행 기록과 재사용
무엇을 어떻게 바꿨는지도 남겼다.
file_put_contents('/var/log/batch-update.log',
sprintf("%s %s cond=%s expect=%d actual=%d by=%s\n",
date('c'), 'product_price', $where, count($rows), $db->affectedRows(), get_current_user()),
FILE_APPEND);
cond 와 expect 와 actual 이 남으니 나중에 이 가격이 언제 바뀌었느냐는 물음에 답할 수 있다.
두 달 뒤에 다른 분류의 가격을 올리게 돼서 이 스크립트를 다시 썼다.
$params = [12]; // ← 분류 번호만 바꾸면 될 것 같았다
그런데 조건에 다른 것이 박혀 있었다.
$where = "category_no = ? AND use_yn = 'Y' AND supplier_no = 3";
지난번에 급하게 넣은 supplier_no = 3 이 $where 에 남아 있었다. 인자만 바꾸고 돌렸으면 대상이 달랐다.
재사용할 때는 인자만이 아니라 조건문 전체를 읽는다. 본문에 박힌 값을 못 보면 엉뚱한 대상이 바뀐다.
정리
- 확인용과 실행용을 따로 쓰면 조건이 달라진다
- 두 벌로 적는 순간 어긋날 자리가 생긴다
- 조건을 한 곳에 두고 확인과 실행이 같은 것을 쓰게 한다
- 확인에서 센 건수와 실제 바뀐 건수를 비교한다
- 되돌릴 것을 먼저 만든다. 백업도 같은 조건을 따른다
- 사람이 판단하는 동안 잠그면 서비스가 멈춘다. 건수에 따라 방식을 고른다
- 무엇을 어떤 조건으로 몇 건 바꿨는지 남긴다
- 재사용할 때
WHERE본문에 박힌 값을 다시 본다