Study/Node.JS

[Node.js / Express 5] SQL 기초 문법과 정규화

the.Dev.Cat 2026. 3. 25. 12:51
SQL 기초 문법과 정규화

SQL 분류

SQL은 기능에 따라 네 가지로 분류된다. 각 분류가 다루는 대상이 다르므로 구분해서 이해하는 것이 중요하다.

분류 전체 이름 대상 주요 명령
DDL Data Definition Language 테이블·스키마 구조 CREATE, ALTER, DROP, TRUNCATE
DML Data Manipulation Language 테이블 안의 데이터 SELECT, INSERT, UPDATE, DELETE
DCL Data Control Language 사용자 권한 GRANT, REVOKE
TCL Transaction Control Language 트랜잭션 흐름 COMMIT, ROLLBACK, SAVEPOINT

DDL — 구조 정의

DDL(Data Definition Language)은 테이블 구조를 정의하거나 수정하는 문법이다.

CREATE DATABASE my_db DEFAULT CHARACTER SET utf8mb4; USE my_db; ALTER TABLE member ADD COLUMN age INT; ALTER TABLE member DROP COLUMN age; ALTER TABLE member CHANGE name full_name VARCHAR(30); ALTER TABLE member MODIFY COLUMN status VARCHAR(20); DROP TABLE IF EXISTS member; TRUNCATE TABLE member;

TRUNCATE는 데이터만 전체 삭제하며 복구가 불가능하다. DROP과 달리 테이블 구조는 남는다.


DML — 데이터 조작

DML(Data Manipulation Language)은 데이터를 조회하고 변경하는 문법이다.

INSERT 구문

테이블에 새 행을 삽입한다. 칼럼 목록을 명시하면 순서가 달라도 안전하고, 생략하면 테이블 정의 순서대로 값을 지정해야 한다.

INSERT INTO member (name, email, status) VALUES ('홍길동', 'hong@example.com', 'ACTIVE'); INSERT INTO member (name, email, status) VALUES ('김철수', 'chul@example.com', 'ACTIVE'), ('이영희', 'young@example.com', 'INACTIVE');

여러 행을 한 번에 삽입할 때는 VALUES 뒤에 콤마로 구분하여 나열한다. 단건 INSERT를 반복하는 것보다 성능이 좋다.

SELECT 실행 순서

SQL은 작성 순서와 실행 순서가 다르다.

FROM -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY

SELECTFROM보다 나중에 실행되므로, WHERE 절에서는 SELECT에서 지정한 별칭(alias)을 사용할 수 없다.

주요 조회 패턴

SELECT name, email FROM member WHERE status = 'ACTIVE' ORDER BY created_at DESC LIMIT 10 OFFSET 20; SELECT category_id, COUNT(*) AS book_count FROM book GROUP BY category_id HAVING COUNT(*) > 5; SELECT * FROM member WHERE phone IS NULL; SELECT DISTINCT category_id FROM book;

NULL 검사는 = NULL이 아니라 IS NULL을 사용한다. WHERE phone = NULL은 항상 false를 반환한다.

UPDATE 구문

UPDATE member SET name = '홍길순' WHERE id = 1; UPDATE member SET status = 'INACTIVE', updated_at = NOW() WHERE last_login_at < DATE_SUB(NOW(), INTERVAL 1 YEAR);

UPDATE에서 WHERE 절을 빠뜨리면 테이블 전체 행이 수정된다. 주의가 필요한 부분이다.

DELETE 구문

특정 조건에 맞는 행을 삭제한다. WHERE 절 없이 실행하면 테이블의 모든 행이 삭제된다.

DELETE FROM member WHERE id = 1; DELETE FROM member WHERE status = 'INACTIVE' AND last_login_at < DATE_SUB(NOW(), INTERVAL 2 YEAR);

TRUNCATE와 달리 DELETE는 트랜잭션 안에서 실행하면 롤백이 가능하다. FK 제약이 있는 경우 참조되는 행은 삭제가 거부된다.

집계 함수

GROUP BY와 함께 자주 쓰이는 집계 함수다.

함수 설명 NULL 처리
COUNT(*) 전체 행 수 NULL 포함
COUNT(col) 해당 칼럼이 NULL이 아닌 행 수 NULL 제외
SUM(col) 합계 NULL 제외
AVG(col) 평균 NULL 제외
MAX(col) 최댓값 NULL 제외
MIN(col) 최솟값 NULL 제외
SELECT category_id, COUNT(*) AS total_books, AVG(price) AS avg_price, MAX(price) AS max_price, MIN(price) AS min_price FROM book GROUP BY category_id;

WHERE vs HAVING 차이

둘 다 필터링 조건이지만 동작하는 시점이 다르다.

구분 WHERE HAVING
실행 시점 GROUP BY 이전 GROUP BY 이후
필터 대상 개별 행 그룹(집계 결과)
집계 함수 사용 불가 가능
사용 예 WHERE status = 'ACTIVE' HAVING COUNT(*) > 5
SELECT category_id, COUNT(*) AS book_count FROM book WHERE price > 10000 GROUP BY category_id HAVING COUNT(*) >= 3;

WHERE price > 10000으로 먼저 행을 줄이고, GROUP BYHAVING COUNT(*) >= 3으로 그룹을 필터링한다. 성능 최적화를 위해 가능하면 WHERE로 먼저 걸러내는 것이 좋다.


JOIN — 테이블 합치기

JOIN은 두 개 이상의 테이블을 연결해 데이터를 조회하는 방법이다.

INNER JOIN

두 테이블 모두에 일치하는 행만 반환한다. 교집합이다.

SELECT m.name, b.title, mb.rental_date FROM member m INNER JOIN member_book mb ON m.id = mb.member_id INNER JOIN book b ON mb.book_id = b.id;

LEFT OUTER JOIN

왼쪽 테이블을 기준으로 오른쪽 테이블과 연결한다. 오른쪽에 매칭되는 행이 없으면 NULL로 채운다.

SELECT m.name, b.title FROM member m LEFT JOIN member_book mb ON m.id = mb.member_id LEFT JOIN book b ON mb.book_id = b.id;

대여 이력이 없는 회원도 결과에 포함된다.

EXCLUSIVE JOIN

LEFT JOIN 후 오른쪽 테이블이 NULL인 행만 필터링한다. 한쪽에만 존재하는 데이터를 찾을 때 사용한다.

SELECT m.* FROM member m LEFT JOIN member_book mb ON m.id = mb.member_id WHERE mb.member_id IS NULL;

대여 이력이 없는 회원만 조회된다.

SELF JOIN

같은 테이블을 두 번 조인한다. 추천인처럼 같은 테이블 내에 참조 관계가 있을 때 사용한다.

SELECT m.name AS '회원', r.name AS '추천인' FROM member m LEFT JOIN member r ON m.referrer_id = r.id;

FULL OUTER JOIN

두 테이블 중 한쪽에만 있는 행도 모두 포함한다. 합집합이다. MySQL은 FULL OUTER JOIN 문법을 지원하지 않으므로 LEFT JOIN과 RIGHT JOIN을 UNION으로 합쳐서 구현한다.

SELECT m.name, b.title FROM member m LEFT JOIN member_book mb ON m.id = mb.member_id LEFT JOIN book b ON mb.book_id = b.id UNION SELECT m.name, b.title FROM member m RIGHT JOIN member_book mb ON m.id = mb.member_id RIGHT JOIN book b ON mb.book_id = b.id;

UNION은 기본적으로 중복을 제거한다. 중복을 포함하고 싶다면 UNION ALL을 사용한다.


서브쿼리 3가지 유형

서브쿼리는 쿼리 안에 포함된 또 다른 쿼리다. 서브쿼리가 먼저 실행되고 메인쿼리가 나중에 실행된다. 반드시 ()로 감싸야 한다.

유형위치특징
중첩 서브쿼리WHERE 절조건값을 SELECT로 가져옴
인라인 뷰FROM 절테이블처럼 사용, 별칭 필수
스칼라 서브쿼리SELECT 절반드시 단일 값 반환
SELECT name, height FROM member WHERE height > (SELECT AVG(height) FROM member); SELECT name FROM member WHERE height > ANY (SELECT height FROM member WHERE gender = 'MALE');
SELECT active_members.name FROM ( SELECT * FROM member WHERE status = 'ACTIVE' ) active_members WHERE active_members.id IN (SELECT DISTINCT member_id FROM member_book);

FROM 절의 서브쿼리(인라인 뷰)는 반드시 별칭을 붙여야 한다.

SELECT m.name, (SELECT COUNT(*) FROM member_book mb WHERE mb.member_id = m.id) AS rental_count FROM member m;

스칼라 서브쿼리는 SELECT 절에 위치하며 반드시 단일 값을 반환해야 한다.

JOIN vs 서브쿼리 비교

같은 결과를 JOIN으로도 서브쿼리로도 작성할 수 있다. 어느 쪽을 선택할지는 가독성, 데이터 규모, 실행 계획을 함께 고려해야 한다.

기준 JOIN 서브쿼리
가독성 관계가 명확하게 드러남 단계별 의도를 분리해서 표현 가능
성능 옵티마이저가 최적화하기 유리 스칼라 서브쿼리는 행마다 실행되어 느릴 수 있음
중복 행 1:N 관계면 중복 발생 가능 EXISTS로 중복 없이 존재 여부만 확인 가능
적합한 상황 여러 칼럼을 함께 조회할 때 집계값을 조건으로 쓸 때, 존재 여부만 확인할 때
SELECT m.name FROM member m WHERE EXISTS ( SELECT 1 FROM member_book mb WHERE mb.member_id = m.id );

EXISTS는 서브쿼리가 한 건이라도 결과를 반환하면 true를 반환하고 탐색을 멈춘다. JOIN보다 중복 없이 존재 여부를 확인할 때 적합하다.


정규화

정규화는 데이터 중복을 제거하고 무결성을 유지하기 위해 테이블 구조를 분리하는 설계 원칙이다.

정규화가 필요한 이유 — 이상(Anomaly) 현상

이상 종류문제
삽입 이상특정 데이터가 없으면 다른 데이터도 삽입 불가
삭제 이상한 데이터를 지우면 다른 데이터도 사라짐
수정 이상한 가지를 바꾸려면 여러 행을 수정해야 함

1NF — 원자값 (반복 그룹 제거)

하나의 칸에는 하나의 값만 저장해야 한다.

-- 위반: 하나의 칸에 여러 값 book_id | tags 1 | "자기계발, 경제, 투자" -- 1NF 적용 후 book_id | tag 1 | 자기계발 1 | 경제 1 | 투자

2NF — 부분 함수 종속 제거

복합 PK가 있을 때, PK 일부에만 종속되는 칼럼은 별도 테이블로 분리한다.

-- 위반: PK (order_id, product_id)인데 product_name은 product_id에만 종속 order_id | product_id | product_name | quantity -- 2NF 적용 후 order_item: order_id, product_id, quantity product: product_id, product_name

3NF — 이행 함수 종속 제거

PK가 아닌 칼럼이 다른 PK가 아닌 칼럼을 결정하는 경우 분리한다.

-- 위반: order_id -> customer_id -> customer_phone (이행 종속) order: order_id, customer_id, customer_phone -- 3NF 적용 후 order: order_id, customer_id customer: customer_id, customer_phone

트랜잭션

트랜잭션은 하나의 논리적 작업 단위로 묶인 SQL 명령들의 집합이다. 전체가 성공하거나 전체가 실패해야 한다. 은행 이체처럼 출금과 입금이 반드시 함께 처리되어야 하는 경우가 대표적인 예다.

ACID 속성

속성 영문 의미
원자성 Atomicity 트랜잭션 내 작업은 전부 성공하거나 전부 실패한다
일관성 Consistency 트랜잭션 전후 DB는 항상 유효한 상태를 유지한다
격리성 Isolation 동시에 실행되는 트랜잭션은 서로 영향을 주지 않는다
지속성 Durability 커밋된 결과는 장애가 발생해도 유지된다

START TRANSACTION / COMMIT / ROLLBACK

START TRANSACTION; UPDATE account SET balance = balance - 10000 WHERE id = 1; UPDATE account SET balance = balance + 10000 WHERE id = 2; COMMIT;
START TRANSACTION; UPDATE account SET balance = balance - 10000 WHERE id = 1; ROLLBACK;

COMMIT을 실행하면 변경 내용이 확정되어 다른 세션에서도 조회된다. ROLLBACK을 실행하면 트랜잭션 시작 이후의 모든 변경이 취소된다. MySQL InnoDB는 기본적으로 autocommit = 1이므로, 명시적으로 트랜잭션을 제어하려면 START TRANSACTION을 선언해야 한다.

SAVEPOINT를 활용하면 트랜잭션 중간 지점을 저장하고, 일부만 롤백하는 것도 가능하다.

START TRANSACTION; INSERT INTO order_log (member_id, action) VALUES (1, 'ORDER_START'); SAVEPOINT before_payment; UPDATE account SET balance = balance - 5000 WHERE id = 1; ROLLBACK TO before_payment; COMMIT;

중복 요청 방지

같은 요청이 두 번 실행됐을 때 데이터가 중복 삽입되는 것을 방지하는 방법이 있다.

방법구현안전도
프론트 디바운스버튼 disabled 처리낮음 (우회 가능)
DB UNIQUE 제약UNIQUE KEY uq_member_mission (member_id, mission_id)높음
멱등키헤더에 UUID 포함, 서버에서 중복 감지높음
비관적 락SELECT FOR UPDATE높음 (성능 주의)

가장 안전하고 단순한 방법은 DB UNIQUE 제약이다. 중복 삽입 자체를 데이터베이스 레벨에서 막는다.

CREATE TABLE mission_attempt ( id BIGINT AUTO_INCREMENT PRIMARY KEY, member_id BIGINT NOT NULL, mission_id BIGINT NOT NULL, status VARCHAR(15) NOT NULL DEFAULT 'CHALLENGING', created_at DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), UNIQUE KEY uq_member_mission (member_id, mission_id), FOREIGN KEY (member_id) REFERENCES member(id), FOREIGN KEY (mission_id) REFERENCES mission(id) );

(member_id, mission_id) 조합이 유일하도록 제약을 걸면, 같은 회원이 같은 미션에 두 번 도전하려고 할 때 DB가 에러를 반환한다. 프론트 처리와 무관하게 데이터 정합성이 보장된다.