infra
Platform

모듈 맵

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

0 / 37 완료

펼치기
0 / 37 완료0%

Database · 07 / 37

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

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

🚨INCIDENT ALERT
HIGH

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

이번 챕터에서 배울 것

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

  • 1SELECT 완전 기초 — WHERE, ORDER BY, LIMIT, DISTINCT, AS
  • 2WHERE 조건의 다양한 연산자 — BETWEEN, IN, LIKE, AND/OR/NOT
  • 3INSERT — 단건과 다건 삽입
  • 4UPDATE의 함정 — WHERE 없는 UPDATE의 재앙
  • 5DELETE vs TRUNCATE vs 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 구조

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

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;
🔍실행 후 확인할 것
  • 대상 행UPDATE/DELETE 전 같은 WHERE로 SELECT한 행 수를 확인합니다.
  • 정렬 기준ORDER BY가 있어 결과 순서가 재현 가능한지 봅니다.
  • 제한 조건LIMIT/OFFSET이 의도한 페이지 범위를 반환하는지 점검합니다.

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 테이블명 (컬럼 목록) 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 후 SELECT로 삽입된 행이 존재하는지 확인합니다.
  • 다건 삽입은 VALUES 구분자가 쉼표인지 세미콜론인지 확인합니다.
  • RETURNING id 절을 추가하면 삽입된 행의 PK를 바로 확인할 수 있습니다.

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조건에 맞는 행가능실행됨느림 (행별 처리)Auto Increment 초기화 안 됨
TRUNCATE테이블 전체 행어려움실행 안 됨빠름 (페이지 단위)FK 있으면 실행 불가, AUTO_INCREMENT 초기화
DROP테이블 자체 (스키마+데이터)불가종속된 뷰·FK도 함께 처리 필요
위험 명령어

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

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으로 나눠서 배치 실행

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

지식 확인

퀴즈 — 5문제

Q1

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

Q2

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

Q3

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

Q4

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

Q5

LIMIT 10 OFFSET 100의 의미는?

0 / 5 답변

🧪 실습으로 확인하기

PostgreSQL 설치 및 기본 설정

초급

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

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

이것도 배워보세요

database입문 · 55
[Database] B-Tree 인덱스의 작동 원리와 인덱스 설계의 핵심 조건
Database 트랙 계속
linux입문 · 30
[Linux] 개발자가 왜 리눅스 서버와 커맨드라인을 반드시 배워야 하는가
Linux 트랙 시작점