Don't test on live

Production is not your sandbox; suspect yourself first.

Résumé bullet

Owned a first-year production outage: ran an unvetted 20K-row query, froze live equipment, drove the root-cause post-mortem.

Deep-dive
  • 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
Code
-- 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.
Say it (English)
  • ·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.
Why it matters — Opendoor / as an engineer
  • Opendoor: never validate at scale against prod; own mistakes fast.
  • Proves: operational maturity + ownership.
  • A 'tell me about a failure' behavioral card.
Trade-offs (what you give up)
  • 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.
Push it further
  • 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는 연습장이 아니다. 문제 생기면 남보다 나를 먼저 의심해라.
  1. 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

    쿼리에 아이디 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. 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. 4

    처음 15~20분 동안 나는 엉뚱한 곳을 봤다. 서버 로그, 배포 내역… 내 쿼리만 빼고 다 의심했다.

    ⚖️ Trade-off: 내 잘못일 수 있다는 생각을 안 하니, 진짜 원인을 못 찾고 시간만 흘렀다.

    ✅ Fix: 결국 내 쿼리가 원인임을 인정하고 직접 신고했다. 그게 제일 빨리 푸는 길이었다.

    🗣 영어로 말해

    My first reaction was wrong - I blamed logs and deploys, not myself.

    checking microphone…

  5. 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. 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. 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. 8

    결국 그 기능은 취소됐고, 요청한 사용자는 기능을 못 받았다. 남은 교훈은 하나다.

    ⚖️ Trade-off: 내 영향력이 부족해서 더 나은 설계를 밀어붙이지 못한 게 아쉽다.

    ✅ Fix: 운영 DB는 내 연습장이 아니다. 문제가 생기면, 경력보다 겸손하게 나부터 의심한다.

    🗣 영어로 말해

    Production is not your sandbox. Humility, not seniority.

    checking microphone…

8단계 영어를 다 말하면 → 이 메커니즘 전체를 영어로 설명할 수 있게 된다.