Production is not your sandbox; suspect yourself first.
Owned a first-year production outage: ran an unvetted 20K-row query, froze live equipment, drove the root-cause post-mortem.
- Problem: untested 20K-row SELECT run on live prod DB
- Cause: assumed reads safe + dev == prod
- Impact: library cache latch contention froze US factory equipment
- First reaction wrong: 15-20 min blaming logs, deploys, not self
- Fix: self-reported, DBA killed session, lock released, <1hr
- Result: feature cancelled, user lost it; rule = suspect self first
-- Wrong: unique values -> hard parse every run, floods cache SELECT * FROM lot WHERE id IN (1,2,3, ...20000); -- Right: bind variables -> one SQL shape -> soft parse SELECT * FROM lot WHERE id IN (:id1, :id2, ...); -- Better at scale: load once, then join INSERT INTO tmp_ids VALUES (:id); -- batched SELECT l.* FROM lot l JOIN tmp_ids t ON l.id = t.id; -- Before any prod query at scale: DBA review first.
- ·First-year failure: I tested a huge query on the production DB.
- ·It held a library cache lock and froze US factory equipment.
- ·My first reaction was wrong - I blamed logs and deploys, not myself.
- ·I self-reported, the DBA killed my session, equipment came back.
- ·Production is not your sandbox. Humility, not seniority.
- Opendoor: never validate at scale against prod; own mistakes fast.
- Proves: operational maturity + ownership.
- A 'tell me about a failure' behavioral card.
- Bind-variable IN-lists are capped (Oracle 1000 elements); 20K still needs splitting or a temp table.
- Temp-table insert-then-join adds a round-trip and DML/redo where a single read used to suffice.
- DBA pre-review on every large query is a human gate: slower iteration, becomes a bottleneck.
- Bind peeking can pick a bad plan for skewed id sets, trading parse cost for plan instability.
- Batch ids in 1000-element chunks via array binding (JDBC setArray / OCI bind arrays).
- Use a global temporary table so staged ids stay session-private and skip redo.
- Add a resource consumer group + statement timeout so a rogue query self-throttles.
- Set cursor_sharing=FORCE or SQL plan baselines to kill literal-driven hard-parse storms.
Understand it step by step (한국어로 이해 → 영어로 말하기)
- 1
운영 DB(진짜 공장이 쓰는 DB)에서, 테스트도 안 해본 큰 조회 쿼리를 그냥 돌려봤다.
⚖️ Trade-off: 두 가지를 잘못 믿었다. (1) 조회(SELECT)는 읽기만 하니까 안전하다. (2) 개발에서 됐으니 운영도 똑같이 될 거다.
✅ Fix: 둘 다 틀렸다. 읽기 쿼리도 DB 내부 자원을 잡을 수 있고, 개발 환경과 운영 환경은 규모가 다르다.
-- A SELECT is not automatically safe at scale: run it on dev/staging first -- (never validate an untested large query against the production DB) EXPLAIN PLAN FOR SELECT * FROM lot WHERE id IN (:id1, :id2);
🗣 영어로 말해“As a first-year engineer, I tested a huge query directly on the production database.”
checking microphone…
- 2
쿼리에 아이디 2만 개를 일일이 숫자로 박았다. DB는 그걸 매번 '처음 보는 새 쿼리' 2만 개로 받아들였다.
⚖️ Trade-off: 값이 다 다르면 DB가 매번 쿼리를 새로 분석(파싱)한다. 2만 번을 한꺼번에 했다.
✅ Fix: DB가 쿼리 정보를 모아두는 공용 공간(library cache)이 꽉 차고, 거기 들어가려는 줄(latch)이 막혔다.
🔧 도구:SQL hard parselibrary cachelatch contention
-- Literal IN-list = 20,000 unique SQL shapes -> hard parse storm, -- floods the library cache and spikes latch contention. SELECT * FROM lot WHERE id IN (1, 2, 3, /* ...20000 literals... */ 20000);
🗣 영어로 말해“Twenty thousand unique queries flooded the shared cache and blocked it.”
checking microphone…
- 3
그 공용 공간이 막히니, 거기를 같이 쓰던 다른 작업들도 멈췄다. 그중에 미국 공장 장비가 있었다.
⚖️ Trade-off: 내 쿼리 하나가 공용 자원을 잡아버려서, 나와 상관없는 장비까지 다 같이 멈췄다.
✅ Fix: DBA가 내 세션을 강제로 끊으니 잠금이 풀리고, 장비가 1시간 안에 돌아왔다.
🔧 도구:DBA kill session
-- Recovery: DBA kills the offending session, releasing the held latch ALTER SYSTEM KILL SESSION ':sid,:serial#' IMMEDIATE;
🗣 영어로 말해“That lock froze US factory equipment for almost an hour.”
checking microphone…
- 4
처음 15~20분 동안 나는 엉뚱한 곳을 봤다. 서버 로그, 배포 내역… 내 쿼리만 빼고 다 의심했다.
⚖️ Trade-off: 내 잘못일 수 있다는 생각을 안 하니, 진짜 원인을 못 찾고 시간만 흘렀다.
✅ Fix: 결국 내 쿼리가 원인임을 인정하고 직접 신고했다. 그게 제일 빨리 푸는 길이었다.
🗣 영어로 말해“My first reaction was wrong - I blamed logs and deploys, not myself.”
checking microphone…
- 5
쉬운 고침: 숫자를 직접 박지 말고 '자리표(바인드 변수)'를 쓴다. 그럼 DB가 같은 모양 쿼리로 보고 한 번만 분석한다.
⚖️ Trade-off: 하지만 자리표 IN 목록은 한 번에 넣을 수 있는 개수가 막혀 있다(오라클은 1000개). 2만 개는 여전히 못 넣는다.
✅ Fix: 그래서 1000개씩 잘라서 여러 번 보내거나(배치), 아예 임시 테이블에 넣고 조인한다.
🔧 도구:bind variablesJDBC setArraycursor_sharing=FORCE
-- Right: bind variables -> one SQL shape -> soft parse (parsed once, not 20K times) SELECT * FROM lot WHERE id IN (:id1, :id2, :id3 /* ... */); -- Oracle caps an IN-list at 1000, so split into 1000-element batches.
🗣 영어로 말해“The real fix is bind variables, so the database parses one query shape, not twenty thousand.”
checking microphone…
- 6
더 큰 규모면: 아이디들을 임시 테이블에 한 번 넣어두고, 그 테이블이랑 조인해서 조회한다.
⚖️ Trade-off: 이건 읽기 한 번이면 됐던 걸, '쓰기(INSERT) + 한 번 더 왕복'으로 만든다. 일이 조금 늘어난다.
✅ Fix: 대신 세션 전용 임시 테이블을 쓰면 그 추가 비용이 작아진다. 대량 처리에선 이게 더 안전하다.
🔧 도구:global temporary tableINSERT then JOIN
-- At scale: stage the ids once, then join (instead of a giant IN-list) INSERT INTO tmp_ids VALUES (:id); -- batched insert SELECT l.* FROM lot l JOIN tmp_ids t ON l.id = t.id;
🗣 영어로 말해“At larger scale I load the ids into a temp table and join instead.”
checking microphone…
- 7
그리고 큰 쿼리는 운영에 돌리기 전에 DBA한테 먼저 검토받는다.
⚖️ Trade-off: 사람이 매번 봐주면 느려진다. DBA가 바쁘면 거기서 막힐 수 있다(병목).
✅ Fix: 그래도 '큰 쿼리'에만 적용하면 된다. 추가로 쿼리에 자동 제한시간을 걸어두면, 위험한 쿼리는 스스로 멈춘다.
🔧 도구:DBA pre-reviewresource consumer groupstatement timeout
-- Self-throttle a rogue query so it can't run unbounded before DBA review ALTER SESSION SET statement_timeout = '30s'; -- Before any large prod query at scale: DBA review first.
🗣 영어로 말해“And I get a DBA to review any large query before it touches production.”
checking microphone…
- 8
결국 그 기능은 취소됐고, 요청한 사용자는 기능을 못 받았다. 남은 교훈은 하나다.
⚖️ Trade-off: 내 영향력이 부족해서 더 나은 설계를 밀어붙이지 못한 게 아쉽다.
✅ Fix: 운영 DB는 내 연습장이 아니다. 문제가 생기면, 경력보다 겸손하게 나부터 의심한다.
🗣 영어로 말해“Production is not your sandbox. Humility, not seniority.”
checking microphone…
8단계 영어를 다 말하면 → 이 메커니즘 전체를 영어로 설명할 수 있게 된다.