infra
Platform

모듈 맵

[Database] 데이터 CRUD를 위한 SELECT, INSERT, UPDATE, DELETE 핵심 기초

0 / 37 완료

펼치기
0 / 37 완료0%

데이터베이스 설계 & 튜닝 · 07 / 37

[Database] 데이터 CRUD를 위한 SELECT, INSERT, UPDATE, DELETE 핵심 기초

데이터 조회, 삽입, 수정, 삭제의 기본 문법과 WHERE 조건을 실습으로 익힙니다

🚨INCIDENT ALERT
HIGH

ORM만 쓰던 백엔드 개발자도 결국 직접 SQL을 확인해야 하는 순간이 옵니다. SELECT, INSERT, UPDATE, DELETE의 기본 동작을 모르면 작은 수정도 위험해집니다. 기본 문법과 안전 습관을 같이 익혀야 실무 쿼리를 자신 있게 다룰 수 있습니다.

이번 챕터에서 배울 것

SELECT, INSERT, UPDATE, DELETE는 매일 사용하는 기본 명령이지만, WHERE 조건 실수로 인한 전체 업데이트/삭제는 실무에서 가장 자주 발생하는 장애 원인 중 하나입니다. 올바른 사용 습관과 안전 패턴을 익혀야 합니다.

  • 1WHERE, ORDER BY, LIMIT, DISTINCT, AS로 기본 SELECT 쿼리를 작성할 수 있다
  • 2BETWEEN, IN, LIKE, AND/OR/NOT 연산자로 WHERE 조건을 구성할 수 있다
  • 3단건과 다건 INSERT로 데이터를 삽입할 수 있다
  • 4WHERE 없는 UPDATE의 위험을 이해하고 안전하게 UPDATE를 작성할 수 있다
  • 5DELETE, TRUNCATE, DROP의 차이를 구분하고 상황에 맞게 사용할 수 있다
  • 6실수를 방지하는 트랜잭션 활용 패턴을 적용할 수 있다

SQL 기초 4종 — SELECT, INSERT, UPDATE, DELETE

ORM만 쓰다가 처음으로 직접 쿼리를 짜야 했던 순간이 있었다. 슬로우 쿼리 알림이 터졌는데 DBA가 "쿼리 보여줘"라고 했고, Sequelize가 생성한 SQL을 보니 WHERE 조건도 이상하고 컬럼 별칭도 뒤죽박죽이었다. 손으로 쿼리를 고쳐보려 했지만 SELECT절의 순서도, WHERE가 언제 평가되는지도 몰랐다. 더 무서웠던 건 그 다음이었다 — 테스트 DB에서 UPDATE users SET status = 'inactive'를 쳤는데 WHERE 조건을 빠뜨린 채로 엔터를 눌렀고, 전체 사용자 상태가 바뀌었다. 그때 처음으로 SQL이 단순한 "ORM 아래 있는 것"이 아니라, 잘못 쓰면 되돌리기 어려운 파괴적인 명령이라는 걸 깨달았다. 이 모듈은 SELECT, INSERT, UPDATE, DELETE의 정확한 동작 방식과, 실수가 일어나기 전에 막는 습관을 함께 가르친다.

SQL(Structured Query Language)의 핵심은 4가지 DML(Data Manipulation Language) 명령입니다. SELECT로 조회하고, INSERT로 삽입하고, UPDATE로 수정하고, DELETE로 삭제합니다. 이 모듈에서는 각 명령의 정확한 문법과 실전에서 자주 만나는 함정을 함께 다룹니다.

파괴적인 쿼리(UPDATE, DELETE)를 실행하기 전에는 반드시 트랜잭션으로 감싸고 SELECT로 대상을 먼저 확인하는 습관을 들이세요. 이것이 이 모듈에서 가장 중요한 안전 원칙입니다.


💡개념

SELECT 완전 기초 — 데이터 조회의 모든 것

API를 만들었는데 데이터를 가져오는 쿼리가 원하는 결과를 반환하지 않습니다. WHERE 조건이 맞는 것 같은데 데이터가 안 나오거나, ORDER BY 없이 매번 순서가 달라집니다. SELECT 구조와 각 절의 실행 순서를 정확히 이해해야 원하는 데이터를 정확하게 조회할 수 있습니다.

SELECT 문 실행 순서 — 작성 순서와 실행 순서가 다르다. 작성은 SELECT→FROM→JOIN→WHERE→GROUP BY→HAVING→ORDER BY→LIMIT 순으로 쓰지만, 실행은 ① FROM/JOIN(테이블 식별·조인) ② WHERE(행 필터링, 인덱스 활용) ③ GROUP BY(그룹 생성) ④ HAVING(그룹 필터링) ⑤ SELECT(컬럼 선택·계산) ⑥ ORDER BY(정렬) ⑦ LIMIT(결과 수 제한) 순이다. SELECT가 거의 마지막에 실행되므로 WHERE에서는 SELECT 별칭(alias)을 쓸 수 없고, ORDER BY는 SELECT 다음이라 별칭 사용이 가능하다확대

기본 SELECT 구조

SELECT 문은 다음 절들을 순서대로 조합해서 사용합니다. FROM으로 대상 테이블을 지정하고, WHERE로 조건을 걸고, ORDER BY로 정렬하고, LIMIT/OFFSET으로 결과 수를 제한합니다.

논리적 실행 순서 — 작성 순서와 다르다

DB는 SQL을 쓴 순서대로 실행하지 않습니다. 논리적 실행 순서는 다음과 같습니다:

TEXT
FROM/JOIN  →  WHERE  →  GROUP BY  →  HAVING  →  SELECT  →  ORDER BY  →  LIMIT/OFFSET
(테이블 결정) (행 필터) (그룹화)   (그룹 필터) (열·별칭) (정렬)     (개수 제한)

이 순서가 실무 함정을 설명합니다:

  • WHERE에서 SELECT 별칭(alias)을 못 쓴다 — WHERE가 SELECT보다 먼저 실행되어 별칭이 아직 없기 때문. 그룹 조건은 WHERE가 아니라 HAVING으로.
  • LIMIT은 "맨 마지막" — 정렬(ORDER BY)까지 끝난 결과에서 N개를 자릅니다. LIMIT 10은 "100ms 안에 10개"가 아니라 "정렬된 결과의 상위 10행"이라는 뜻(성능과 무관한 행 개수 제한).
  • ORDER BY 없는 LIMIT은 무의미 — 정렬이 없으면 어떤 10개가 나올지 보장되지 않습니다(매 실행마다 달라질 수 있음).
1기본 SELECT 구조 실행

SELECT의 기본 구조를 실행해봅니다. FROM → WHERE → ORDER BY → LIMIT 순서로 각 절이 어떤 역할을 하는지 확인합니다.

SQL
SELECT 컬럼명1, 컬럼명2
FROM 테이블명
WHERE 조건
ORDER BY 정렬컬럼 ASC
LIMIT 최대행수
OFFSET 건너뛸행수;
OUTPUT
실행 완료 또는 조회 결과가 표시됩니다.
SELECT id, name, email FROM users WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT 10;
🔍실행 후 확인할 것
  • 반환 행 수 먼저 확인: 결과가 0건이면 WHERE 조건이 매칭되지 않은 것입니다. 특히 deleted_at IS NULL인데 모든 행이 soft-delete 처리된 경우가 잦습니다. 조건을 하나씩 제거해 어디서 필터링되는지 좁히세요.
  • ORDER BY 없이 실행하면 반환 순서는 보장되지 않습니다. 같은 쿼리를 두 번 실행해 순서가 달라지면 ORDER BY가 빠진 것입니다. UPDATE/DELETE 전 SELECT를 항상 먼저 실행해 대상 행 수를 눈으로 확인하는 습관이 중요합니다.
  • LIMIT 10 OFFSET 40인데 행이 3개만 나왔다면 전체 데이터가 43건 미만이라는 뜻입니다. 페이지네이션 구현 시 OFFSET이 전체 행 수를 초과하면 빈 결과가 정상이므로 빈 배열을 에러로 오해하지 마세요.
  • WHERE 조건에 NULL 비교 주의: status != 'cancelled' 조건은 status가 NULL인 행을 제외합니다. NULL이 있는 컬럼을 필터링할 때는 IS NULL / IS NOT NULL을 명시적으로 추가해야 합니다.

WHERE 조건 연산자

WHERE 절에는 다양한 연산자를 조합할 수 있습니다. 아래 표는 자주 쓰는 연산자를 정리한 것입니다.

연산자의미예시
=, !=, <>같다 / 다르다price = 10000
>, >=, <, <=크기 비교price >= 5000
BETWEEN a AND b범위 (경계값 포함)price BETWEEN 10000 AND 50000
IN (...)목록 중 하나status IN ('pending', 'shipped')
LIKE '패턴'문자열 패턴 매칭name LIKE '김%'
IS NULL / IS NOT NULLNULL 여부deleted_at IS NULL
AND, OR, NOT논리 결합role = 'admin' AND active = true

BETWEEN은 경계값을 포함합니다. BETWEEN 10000 AND 50000>= 10000 AND <= 50000과 동일합니다.

LIKE 와일드카드에서 %는 0개 이상의 문자, _는 정확히 한 글자를 의미합니다. 앞에 %가 붙는 패턴('%gmail.com')은 인덱스를 사용할 수 없어 대형 테이블에서 성능 문제가 생깁니다. 접두사 검색('김%')은 인덱스를 활용할 수 있습니다.

NOT IN은 목록에 NULL이 포함되어 있으면 예상치 못한 결과를 반환합니다. NULL과의 비교는 항상 UNKNOWN이 되기 때문입니다.

SQL
SELECT * FROM products WHERE price = 10000;
SELECT * FROM products WHERE price BETWEEN 10000 AND 50000;
SELECT * FROM orders WHERE status IN ('pending', 'processing', 'shipped');
SELECT * FROM users WHERE email LIKE '%@gmail.com';
SELECT * FROM users WHERE name LIKE '김%';
SELECT * FROM users WHERE name LIKE '_길동';
SELECT * FROM products
WHERE category = '전자제품'
  AND price < 100000
  AND stock > 0;
SELECT * FROM users
WHERE (role = 'admin' OR role = 'moderator')
  AND deleted_at IS NULL;

SELECT 실전 예시

아래는 실무에서 자주 쓰이는 SELECT 패턴 5가지입니다. AS 키워드로 컬럼에 별칭을 붙이고, DISTINCT로 중복을 제거하고, LIMIT/OFFSET으로 페이지네이션을 구현합니다. 페이지네이션에서 3페이지(페이지당 20개)를 조회하려면 OFFSET은 (페이지-1) * 페이지크기 = 40이 됩니다.

SQL
SELECT id, name, email, created_at
FROM users
WHERE deleted_at IS NULL
ORDER BY created_at DESC
LIMIT 10;

SELECT
    id,
    name AS 상품명,
    price AS 가격,
    ROUND(price * 0.9, 0) AS 할인가
FROM products
WHERE is_active = true
ORDER BY price DESC
LIMIT 20;

SELECT DISTINCT category
FROM products
WHERE is_active = true
ORDER BY category;

SELECT id, name, price
FROM products
ORDER BY id
LIMIT 20 OFFSET 40;

SELECT *
FROM orders
WHERE created_at >= '2024-01-01'
  AND created_at < '2024-02-01'
ORDER BY created_at DESC;

ORDER BY 정렬

여러 컬럼으로 정렬할 때는 앞에 나온 기준이 먼저 적용됩니다. 아래 예시에서 가격이 같은 상품끼리는 이름 오름차순으로 추가 정렬됩니다.

SQL
SELECT * FROM products ORDER BY price ASC;
SELECT * FROM products ORDER BY price DESC;

SELECT * FROM products
ORDER BY price ASC, name ASC;
💡개념

INSERT / UPDATE / DELETE — 데이터 변경 시 주의할 것들

UPDATE users SET role = 'admin'을 WHERE 없이 실행했습니다. 모든 사용자가 admin이 됩니다. DELETE FROM orders를 실수로 치면 주문 데이터 전체가 사라집니다. DML은 즉시 데이터를 바꾸고 트랜잭션 없이는 복구가 어렵습니다. 데이터 변경 쿼리를 쓸 때의 올바른 패턴을 익혀야 이런 사고를 막을 수 있습니다.

INSERT / UPDATE / DELETE — 데이터 변경 시 주의사항. INSERT는 컬럼을 명시(INSERT INTO users (email, name) VALUES ...)해야 스키마 변경에 안전하고 다중 행도 한 번에 넣는다. UPDATE·DELETE는 반드시 WHERE 조건을 포함해야 하며(WHERE 없는 UPDATE는 전체 테이블 변경, WHERE 없는 DELETE는 TRUNCATE와 같아 되돌리기 어려움), 실행 전 같은 조건으로 SELECT해 대상을 확인한다. 안전 수칙: BEGIN 트랜잭션 시작 → SELECT로 대상 확인 → UPDATE/DELETE 실행 → 결과 확인 후 COMMIT, 이상 시 ROLLBACK확대

INSERT — 데이터 삽입

단건 삽입과 다건 삽입 모두 INSERT INTO 테이블명 (컬럼 목록) VALUES (값 목록) 형식을 사용합니다. 다건 삽입은 VALUES 절에 여러 행을 쉼표로 나열하는 방식으로, 요청 횟수를 줄여 단건 반복 삽입보다 성능상 유리합니다.

2INSERT — 단건·다건 삽입

단건 삽입과 다건 삽입을 모두 실행해봅니다. 다건 삽입이 단건 반복보다 효율적인 이유를 확인합니다.

SQL
INSERT INTO users (name, email, role)
VALUES ('홍길동', 'hong@example.com', 'user');

INSERT INTO products (name, price, category)
VALUES
    ('아이폰 15', 1500000, '전자제품'),
    ('갤럭시 S24', 1200000, '전자제품'),
    ('맥북 프로', 2900000, '컴퓨터');
INSERT INTO users (name, email, role) VALUES ('홍길동', 'hong@example.com', 'user');
🔍실행 후 확인할 것
  • INSERT 성공 후 "INSERT 0 1" 메시지: 첫 번째 숫자(0)는 OID, 두 번째(1)는 삽입된 행 수입니다. 다건 INSERT에서 "INSERT 0 3"이 나와야 하는데 "INSERT 0 1"이면 VALUES 절에 쉼표 대신 세미콜론을 썼을 가능성이 높습니다.
  • ERROR 23505 (unique_violation): UNIQUE 제약이 걸린 컬럼(email 등)에 이미 존재하는 값을 INSERT하려 할 때 발생합니다. 해결책은 INSERT INTO ... ON CONFLICT DO NOTHING 또는 ON CONFLICT DO UPDATE 패턴을 사용하세요.
  • RETURNING id를 붙이면 삽입된 행의 PK가 즉시 반환됩니다. 이 값이 없으면 LASTVAL() 또는 currval('시퀀스명')으로 조회할 수 있지만, 다중 삽입 환경에서는 RETURNING이 훨씬 안전합니다.

UPDATE — 데이터 수정

UPDATE에서 WHERE 절은 선택이 아니라 필수입니다. WHERE 없이 실행하면 테이블의 모든 행이 변경됩니다. UPDATE를 실행하기 전에 동일한 WHERE 조건으로 SELECT를 먼저 실행해 대상 행을 확인하는 습관을 들이세요.

3UPDATE 전 SELECT로 대상 확인

UPDATE 실행 전에 항상 동일한 WHERE 조건으로 SELECT를 먼저 실행합니다. 대상 행의 수와 내용을 확인한 뒤 안전하게 UPDATE합니다.

SQL
SELECT id, name, email FROM users WHERE id = 42;

UPDATE users
SET email = 'newemail@example.com',
    updated_at = NOW()
WHERE id = 42;

UPDATE products
SET price = price * 0.9,
    updated_at = NOW()
WHERE category = '전자제품'
  AND stock > 0;
SELECT id, name, email FROM users WHERE id = 42;
🔍실행 후 확인할 것
  • UPDATE 전 SELECT에서 예상한 행 수와 UPDATE 후 영향받은 행 수가 일치하는지 확인합니다.
  • 영향받은 행 수가 0이면 WHERE 조건이 잘못된 것입니다.
  • 대용량 UPDATE는 LIMIT으로 나눠서 배치 실행합니다.

DELETE vs TRUNCATE vs DROP

세 명령은 모두 데이터를 제거하지만 범위와 안전성이 크게 다릅니다.

명령어대상롤백 가능트리거 실행속도비고
DELETE조건에 맞는 행DBMS·트랜잭션에 따라 다름DBMS·트리거 종류에 따라 다름행 단위라 보통 느림시퀀스 동작은 DBMS별 확인
TRUNCATE테이블 전체 행DBMS·설정에 따라 다름일반 DELETE 트리거와 동작이 다를 수 있음보통 빠름FK·시퀀스 동작은 DBMS별 확인
DROP테이블 자체 (스키마+데이터)불가종속된 뷰·FK도 함께 처리 필요
위험 명령어

운영 데이터에 적용하면 되돌리기 어려운 변경입니다. 실행 전 대상 테이블, WHERE 조건, 백업 또는 롤백 경로를 반드시 확인하세요.

DBMS별 주의: BEGIN으로 감싼다고 모든 DBMS에서 TRUNCATE가 되돌아가는 것은 아닙니다. PostgreSQL은 일반적으로 트랜잭션 안에서 TRUNCATE를 롤백할 수 있지만, Oracle은 DDL 자동 커밋으로 앞선 DML까지 확정할 수 있고, MySQL은 스토리지 엔진·설정에 따라 동작이 다릅니다. 아래 TRUNCATE·DROP 예시는 격리된 실습 스키마에서만 실행하고, 사용 중인 DBMS 공식 문서에서 롤백·트리거·FK 동작을 먼저 확인하세요.

SQL
-- 아래 명령은 운영 테이블이 아닌 격리된 실습 스키마에서만 실행합니다.
BEGIN;
DELETE FROM cart_items WHERE user_id = 42;
COMMIT;

TRUNCATE TABLE temp_import_data;

DROP TABLE IF EXISTS old_log_table;

실수 방지 트랜잭션 패턴

파괴적인 쿼리를 실행할 때는 BEGIN으로 트랜잭션을 시작하고, 변경 결과를 SELECT로 확인한 뒤 문제가 없으면 COMMIT, 이상하면 ROLLBACK합니다. DELETE 전에 SELECT로 삭제 대상 건수를 미리 확인해두면 실행 후 예상 건수와 비교할 수 있습니다.

4트랜잭션으로 안전한 UPDATE 실행

BEGIN으로 트랜잭션을 시작하고 UPDATE 후 SELECT로 결과를 검증합니다. 이상이 없으면 COMMIT, 문제가 있으면 ROLLBACK합니다.

SQL
BEGIN;

UPDATE orders
SET status = 'cancelled'
WHERE created_at < '2024-01-01' AND status = 'pending';

SELECT COUNT(*), status FROM orders
WHERE created_at < '2024-01-01'
GROUP BY status;

COMMIT;
BEGIN;
🔍실행 후 확인할 것
  • BEGIN 이후 UPDATE 영향 행 수가 예상과 일치하는지 확인합니다.
  • SELECT로 변경 결과를 눈으로 검증한 뒤 COMMIT합니다.
  • 예상과 다르면 즉시 ROLLBACK을 실행합니다.
5DELETE 전 대상 건수 확인

DELETE 실행 전 SELECT COUNT(*)로 삭제 대상 건수를 먼저 파악합니다. 예상 건수와 일치하면 트랜잭션으로 감싸서 DELETE합니다.

SQL
SELECT COUNT(*) FROM logs WHERE created_at < NOW() - INTERVAL '90 days';

BEGIN;
DELETE FROM logs WHERE created_at < NOW() - INTERVAL '90 days';
COMMIT;
SELECT COUNT(*) FROM logs WHERE created_at < NOW() - INTERVAL '90 days';
🔍실행 후 확인할 것
  • SELECT COUNT(*) 결과와 DELETE 후 영향 행 수가 일치하는지 확인합니다.
  • 대용량 삭제는 LIMIT을 붙여 배치로 나눠서 실행합니다.
  • 실수로 COMMIT한 경우 복구하려면 백업이 필요하므로 운영 DB에서는 신중히 실행합니다.

WHERE 절 없는 UPDATE는 문법 오류가 아닙니다. 데이터베이스는 아무 경고 없이 테이블의 모든 행을 수정합니다. 이것이 SQL에서 가장 파괴적인 실수 중 하나인 이유입니다.

예방 방법:

  1. UPDATE 실행 전 동일한 WHERE 조건으로 SELECT를 먼저 실행해 대상 행을 눈으로 확인합니다.
  2. BEGIN으로 트랜잭션을 시작하고 영향받은 행 수(ROW_COUNT)를 확인한 뒤 COMMIT합니다.
  3. 일부 DB 클라이언트(MySQL Workbench 등)의 "Safe Mode"를 활성화하면 WHERE 없는 UPDATE/DELETE를 차단합니다.
  4. 운영 DB에서는 가능한 한 최소 권한 계정을 사용하고, 대규모 변경은 반드시 동료 리뷰를 거칩니다.
💼
실무 맥락이커머스 정산 배치 — 특정 기간 주문 상태 일괄 업데이트
현업 패턴

월말 정산 배치 작업에서 "2024년 1월 이전 미처리 주문을 모두 'expired'로 변경"하는 요구사항이 생겼습니다. 개발자가 WHERE 조건에 날짜 범위만 넣고 status = 'pending' 필터를 빠뜨리면, 이미 배송 완료된 주문까지 상태가 바뀌어 고객 CS 폭주와 데이터 복구 작업이 필요해집니다.

실무 체크리스트:

  • UPDATE 전 동일 WHERE로 SELECT COUNT(*) 실행 → 예상 건수 확인
  • BEGIN으로 감싸고 변경 후 SELECT로 샘플 확인
  • 스테이징 DB에서 먼저 실행 후 운영 적용
  • 대용량(10만 건 이상)은 LIMIT으로 나눠서 배치 실행

심화 — 안전장치에도 비용이 있다: 열어 둔 트랜잭션의 락

💡개념

심화: BEGIN 뒤 잊어버린 COMMIT — idle in transaction의 함정

이 모듈이 가르친 안전 습관은 옳습니다 — 파괴적 UPDATE/DELETE는 BEGIN으로 감싸고, SELECT로 확인한 뒤 COMMIT 또는 ROLLBACK 한다. 그런데 이 안전장치에는 초급을 벗어날 때 꼭 알아야 할 비용이 하나 붙어 있습니다.

BEGIN 안에서 UPDATE가 실행되는 순간, DB는 변경한 모든 행에 쓰기 락을 걸고 COMMIT/ROLLBACK이 올 때까지 유지합니다. 즉 "SELECT로 천천히 확인하는" 그 시간 동안, 당신은 그 행들을 잠근 채로 붙들고 있는 것입니다. 같은 행을 바꾸려는 다른 세션은 그동안 대기합니다.

  • idle in transaction의 위험: BEGIN과 COMMIT 사이에 딴짓을 하면(창을 바꾸거나 자리를 비우면) 세션이 'idle in transaction' 상태로 남습니다. 락은 계속 걸려 있고, 그 행을 기다리는 다른 요청들이 커넥션을 붙든 채 쌓여 결국 커넥션 풀이 고갈될 수 있습니다.
  • VACUUM까지 막는다(PostgreSQL): 오래 열린 트랜잭션은 회수 경계(xmin horizon)를 붙잡아, 그 시간 동안 DB 전체에서 죽은 튜플을 청소하지 못하게 만듭니다. 잠깐의 방치가 테이블 팽창으로 이어집니다.
  • 그래서 규율이 함께 온다: 안전 습관의 짝은 "BEGIN→COMMIT 구간을 짧게"입니다. 확인은 빠르게 끝내고 곧바로 커밋하거나 롤백하세요. 커피 마시러 가면서 트랜잭션을 열어 두면 안 됩니다. 대량 변경은 하나의 긴 트랜잭션보다 짧은 배치 여러 개로 나눕니다.

정리하면, "트랜잭션으로 감싸라"는 옳은 조언이지만 "감싼 채 오래 붙들지 마라"가 반드시 따라와야 합니다. 안전장치가 오히려 서비스를 멈추는 락으로 바뀌는 지점이 바로 여기입니다.

상황: 배운 대로 위험한 UPDATE를 BEGIN으로 감싸고, 확인용 SELECT까지 실행한 참에 급히 회의에 불려 갔습니다. COMMIT도 ROLLBACK도 안 한 채로요. 돌아와 보니 그 시간대에 같은 데이터를 건드리는 다른 사용자들의 저장이 전부 멈췄고, "커넥션 풀 고갈" 알림이 쌓여 있었습니다.

원인: 열어 둔 트랜잭션이 UPDATE로 바꾼 행들의 쓰기 락을 쥔 채 방치됐습니다(idle in transaction). 그 행을 바꾸려던 다른 요청들이 락을 기다리며 각자 커넥션을 붙들고 쌓여 풀이 가득 찼습니다. PostgreSQL이라면 같은 시간에 VACUUM도 정리를 멈춘 상태였습니다.

진단: 방치된 트랜잭션과 막힌 세션을 찾습니다.

SQL
-- 오래 열려 있는 idle in transaction 세션
SELECT pid, state, now() - xact_start AS tx_age,
       wait_event_type, left(query, 50) AS last_q
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY tx_age DESC;

-- 누가 누구를 막고 있는지
SELECT pid, pg_blocking_pids(pid) AS blocked_by, left(query, 50) AS q
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;

tx_age가 수십 분인 'idle in transaction' 세션 하나와, 그 세션에 막혀(blocked_by에 그 pid가 든) 대기 중인 여러 세션이 보이면 확정입니다.

해결: 방치된 세션을 즉시 정리하고, 재발을 구조로 막습니다.

SQL
-- 급한 처치: 방치된 트랜잭션을 끝내 락을 푼다 (그 세션에서)
ROLLBACK;   -- 또는 확인 결과가 맞다면 COMMIT

-- 세션에 접근 못 하면 관리자가 종료
SELECT pg_terminate_backend(<해당_pid>);

-- 재발 방지: 방치된 트랜잭션을 DB가 자동으로 끊게 설정
-- postgresql.conf 또는 세션 단위
SET idle_in_transaction_session_timeout = '30s';

근본 예방은 습관입니다 — 확인은 빠르게 끝내고 곧바로 COMMIT/ROLLBACK 해 트랜잭션 구간을 짧게 유지하고, 자리를 비울 일이 있으면 절대 트랜잭션을 열어 둔 채 두지 않습니다. idle_in_transaction_session_timeout을 걸어 두면 사람이 실수해도 DB가 방치된 트랜잭션을 자동으로 중단해 락을 풀어 줍니다. 대량 변경은 하나의 긴 트랜잭션 대신 짧은 배치로 나눠 락 보유 시간을 최소화합니다.


명령어·구문 빠른 참조

이 모듈에서 다룬 CRUD 기본 구문을 실전 조합과 함께 모았습니다. "예" 열을 그대로 응용해도 됩니다.

구문/명령용도
SELECT … FROM열·테이블 지정 조회SELECT id, name, email FROM users
WHERE행 필터 조건WHERE deleted_at IS NULL AND role = 'admin'
BETWEEN a AND b범위(경계값 포함)WHERE price BETWEEN 10000 AND 50000
IN (…)목록 중 하나WHERE status IN ('pending', 'shipped')
LIKE문자열 패턴(접두사만 인덱스)WHERE name LIKE '김%'
ORDER BY … LIMIT / OFFSET정렬·페이지네이션ORDER BY created_at DESC LIMIT 10 OFFSET 40
DISTINCT중복 제거SELECT DISTINCT category FROM products
INSERT INTO … VALUES단건·다건 삽입INSERT INTO users (name, email) VALUES ('홍','h@x.com'), ('김','k@x.com')
ON CONFLICT DO …중복 시 무시/갱신INSERT … ON CONFLICT (email) DO NOTHING
… RETURNING삽입 행 값 즉시 반환INSERT … RETURNING id
UPDATE … SET … WHERE조건부 수정(WHERE 필수)UPDATE users SET email = ? WHERE id = 42
DELETE FROM … WHERE조건부 삭제DELETE FROM logs WHERE created_at < NOW() - INTERVAL '90 days'
TRUNCATE / DROP TABLE전체 행 즉시 삭제 / 테이블 제거TRUNCATE TABLE temp_import
BEGIN / COMMIT / ROLLBACK트랜잭션으로 안전 실행BEGIN; UPDATE …; SELECT …; COMMIT;

관련 모듈로 더 깊이:

다음 모듈에서는 트랜잭션의 ACID 속성과 BEGIN/COMMIT/ROLLBACK으로 안전한 데이터 처리를 구현하는 방법을 다룹니다.

지식 확인

퀴즈 — 8문제

Q1

WHERE 절 없이 UPDATE를 실행하면 어떻게 되는가?

Q2

DELETE와 TRUNCATE의 가장 중요한 차이는?

Q3

LIKE '%검색어%' 패턴의 성능 문제는?

Q4

orders 테이블에서 지금까지 주문이 발생한 국가 목록(중복 없이)을 뽑으려 합니다. orders 테이블에는 같은 country가 수백 번 반복됩니다. 올바른 쿼리는?

Q5

LIMIT 10 OFFSET 100의 의미는?

Q6

SELECT 문을 쓴 순서(SELECT … FROM … WHERE … GROUP BY … ORDER BY)와 DB가 실제로 처리하는 논리적 순서가 다른데, 그 순서로 인해 생기는 흔한 제약은?

Q7

[심화] UPDATE를 BEGIN으로 감싸 실행한 뒤 COMMIT이나 ROLLBACK 없이 트랜잭션을 열어 둔 채로 두면, 그 사이 무슨 일이 생기나?

Q8

[심화] 큰 UPDATE를 BEGIN으로 감싼 채 자리를 비운 사이 다른 사용자들의 쓰기가 줄줄이 멈추고 커넥션이 가득 찼다. 올바른 대응은?

0 / 8 답변

🧪 실습으로 확인하기

PostgreSQL 설치 및 기본 설정

초급

Linux 서버에 PostgreSQL을 설치하고, 데이터베이스와 사용자를 생성한 뒤 외부 접속이 가능하도록 설정한다.

50📋 5단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요