Skip to content
isdnetworks
Go back

미리 본 것과 실제로 돌린 것의 범위가 달랐다

가격을 일괄로 바꾸는 작업이었다. 미리 뽑아 본 것은 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

SELECTUPDATE 를 따로 썼기 때문이다. 확인용을 먼저 쓰고 나중에 실행용을 새로 쓰면서 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);

condexpectactual 이 남으니 나중에 이 가격이 언제 바뀌었느냐는 물음에 답할 수 있다.

두 달 뒤에 다른 분류의 가격을 올리게 돼서 이 스크립트를 다시 썼다.

$params = [12];   // ← 분류 번호만 바꾸면 될 것 같았다

그런데 조건에 다른 것이 박혀 있었다.

$where = "category_no = ? AND use_yn = 'Y' AND supplier_no = 3";

지난번에 급하게 넣은 supplier_no = 3$where 에 남아 있었다. 인자만 바꾸고 돌렸으면 대상이 달랐다.

재사용할 때는 인자만이 아니라 조건문 전체를 읽는다. 본문에 박힌 값을 못 보면 엉뚱한 대상이 바뀐다.

정리


Share this post on:

Previous Post
통계 화면이 시간 초과로 죽었다
Next Post
목록 한 화면에 쿼리가 백 번 나갔다