본문으로 건너뛰기
김신건의 로그

[SQL] DML

· 수정 · 📖 약 3분 · 995자/단어 #sql #dml #select #insert #update #delete #database
SQL DML, DML, Data Manipulation Language, SELECT, INSERT, UPDATE, DELETE, Upsert

정의

DML (Data Manipulation Language) 은 데이터 자체를 조회/삽입/수정/삭제 하는 SQL 의 하위 집합. 일상 쿼리의 99% 가 여기에 속한다.

명령의미
SELECT데이터 조회
INSERT새 행 삽입
UPDATE기존 행 수정
DELETE행 삭제
MERGEUPSERT (표준), dialect 마다 대안

전체 SQL 체계는 SQL 참고. 스키마 정의는 SQL DDL, 권한·트랜잭션은 SQL DCL TCL.

SELECT, 전체 골격

SELECT [DISTINCT] 컬럼1, 컬럼2, 표현식 AS alias
FROM 테이블 [AS] 별칭
[JOIN ...]                  -- 결합
[WHERE  조건]               -- 행 필터
[GROUP BY 컬럼]              -- 그룹 분할
[HAVING 그룹조건]            -- 그룹 필터
[ORDER BY 컬럼 [ASC|DESC]]  -- 정렬
[LIMIT n [OFFSET m]];       -- 페이징

논리적 실행 순서는 SQL 의 “쿼리 실행의 논리적 순서” 참고.

SELECT 기본 예제

sql
-- 전체
SELECT * FROM users;

-- 컬럼 선택 + 별칭
SELECT id, name AS user_name, salary * 12 AS yearly
FROM users;

-- 필터 + 정렬 + 페이징
SELECT name, salary
FROM users
WHERE city = 'Seoul' AND salary >= 3000
ORDER BY salary DESC
LIMIT 5;

-- 중복 제거
SELECT DISTINCT city FROM users;
결과
-- users 전체
id |  name   | city  | salary
----+---------+-------+--------
1 | Alice   | Seoul |   3000
2 | Bob     | Busan |   4000
3 | Charlie | Seoul |   3500

-- Seoul + 3000 이상, 내림차순
name   | salary
---------+--------
Charlie |   3500
Alice   |   3000

WHERE 의 연산자

연산의미
=, <> (!=), <, <=, >, >=비교
AND, OR, NOT부울
BETWEEN a AND b>= a AND <= b
IN (...)집합 멤버
LIKE / ILIKE (PG)패턴 (%, _)
IS NULL / IS NOT NULLNULL 검사
EXISTS (subquery)결과 존재 여부

IMPORTANT

NULL 비교는 = 가 안 통한다. WHERE col = NULL 은 항상 거짓. WHERE col IS NULL 을 써야 한다. SQL 의 “NULL 의 3-값 논리” 참고.

LIKE 패턴

WHERE name LIKE 'A%'      -- A 로 시작
WHERE name LIKE '%son'    -- son 으로 끝
WHERE name LIKE '_lice'   -- 한 글자 + lice
WHERE name LIKE '%50\\%%' ESCAPE '\\'   -- 리터럴 % 검색

SELECT 표현식

산술, 문자열, 날짜 함수, CASE WHEN. 자세한 타입별 연산은 SQL Data Types 참고.

SELECT
  name,
  salary,
  salary * 12               AS yearly,
  CASE
    WHEN salary >= 4000 THEN 'high'
    WHEN salary >= 3000 THEN 'mid'
    ELSE                     'low'
  END AS tier,
  EXTRACT(YEAR FROM created_at) AS join_year
FROM users;

ORDER BY 의 디테일

ORDER BY salary DESC                          -- 단일 컬럼
ORDER BY city ASC, salary DESC                -- 여러 컬럼
ORDER BY 2 DESC                               -- SELECT 절 N 번째 컬럼 (가독성 나쁨)
ORDER BY salary DESC NULLS LAST               -- NULL 위치 지정 (PG 표준)

dialect 차이:

  • PostgreSQL: 기본 NULLS LAST for DESC, NULLS FIRST for ASC. 명시 가능.
  • MySQL: 기본 NULLS FIRST. NULLS LAST 미지원, ORDER BY col IS NULL, col 우회.

LIMIT / OFFSET 의 함정

SELECT * FROM events ORDER BY id LIMIT 20 OFFSET 1000000;

OFFSET스킵하기 위해 그 행들을 다 읽는다. 깊은 페이지에서 매우 느림.

해법: Keyset Pagination (cursor).

-- 첫 페이지
SELECT * FROM events WHERE id > 0 ORDER BY id LIMIT 20;
-- 다음 페이지: 마지막 id 를 cursor 로
SELECT * FROM events WHERE id > 12345 ORDER BY id LIMIT 20;

서브쿼리

스칼라 서브쿼리

SELECT
  name,
  salary,
  (SELECT AVG(salary) FROM users) AS overall_avg
FROM users;

IN / EXISTS

SELECT name FROM users
WHERE id IN (SELECT user_id FROM orders WHERE amount > 1000);

SELECT name FROM users u
WHERE EXISTS (
  SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.amount > 1000
);

옵티마이저는 보통 EXISTS 가 더 빠르다 (첫 매칭에서 짧게 끊음). IN 은 결과 집합 전체를 만든다.

상관 서브쿼리 (Correlated)

SELECT name,
  (SELECT MAX(amount) FROM orders o WHERE o.user_id = u.id) AS max_order
FROM users u;

외부 쿼리의 행마다 내부 쿼리가 실행, 비용 큼. JOIN 으로 풀 수 있는지 검토.

CTE (Common Table Expression)

WITH 절로 이름 붙인 임시 결과셋. 가독성 + 재사용.

WITH high_paid AS (
  SELECT id, name FROM users WHERE salary >= 4000
),
their_orders AS (
  SELECT u.name, o.amount
  FROM high_paid u
  JOIN orders o ON o.user_id = u.id
)
SELECT name, SUM(amount) AS total
FROM their_orders
GROUP BY name;

PG 12+, MySQL 8+ 에서 잘 동작. 옵티마이저가 inline 화 (PG 12+) 또는 머티리얼라이즈 (PG 11-).

재귀 CTE

WITH RECURSIVE category_tree AS (
  SELECT id, parent_id, name, 1 AS depth
  FROM categories
  WHERE parent_id IS NULL
  UNION ALL
  SELECT c.id, c.parent_id, c.name, ct.depth + 1
  FROM categories c
  JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT * FROM category_tree ORDER BY depth, name;

조직도, 카테고리, BFS/DFS 트리 순회에.

INSERT

단일/다중 행

INSERT INTO users (name, city, salary) VALUES ('Alice', 'Seoul', 3000);

INSERT INTO users (name, city, salary) VALUES
  ('Alice',   'Seoul', 3000),
  ('Bob',     'Busan', 4000),
  ('Charlie', 'Seoul', 3500);

INSERT … SELECT

INSERT INTO users_backup (id, name, city, salary)
SELECT id, name, city, salary FROM users WHERE created_at < '2024-01-01';

RETURNING (PostgreSQL)

INSERT/UPDATE/DELETE 의 결과를 즉시 반환. 자동 생성된 PK 받기에 좋다.

INSERT INTO users (name, city, salary)
VALUES ('Diana', 'Seoul', 3200)
RETURNING id, created_at;

MySQL 은 RETURNING 미지원. LAST_INSERT_ID() 함수로 마지막 PK 만 조회 가능. MariaDB 는 지원.

UPDATE

UPDATE users
SET salary = salary * 1.1,
    updated_at = NOW()
WHERE city = 'Seoul';

다른 테이블 참조해서 UPDATE

UPDATE users u
SET total_spent = sub.total
FROM (
SELECT user_id, SUM(amount) AS total
FROM orders
GROUP BY user_id
) sub
WHERE u.id = sub.user_id;
결과
-- users.total_spent 가 각 사용자의 누적 주문액으로 갱신됨
-- 두 dialect 모두 같은 결과
UPDATE 1234

WHERE 없는 UPDATE 의 무서움

UPDATE users SET salary = 0;     -- ⚠️ 전체 행 갱신!

운영 DB 에선 항상 BEGINSELECT 로 검증 → UPDATECOMMIT 의 흐름. SQL DCL TCL 참고.

DELETE

DELETE FROM users WHERE id = 5;
DELETE FROM users WHERE created_at < '2020-01-01';

다른 테이블 참조해서 DELETE

-- PostgreSQL
DELETE FROM users u
USING blacklist b
WHERE u.email = b.email;

-- MySQL
DELETE u FROM users u
JOIN blacklist b ON u.email = b.email;

DELETE vs TRUNCATE

DELETETRUNCATE
구분DMLDDL
WHERE✗ (전체만)
트리거동작동작 안 함
시퀀스 리셋옵션 (PG)
속도행 단위, 느림페이지 단위, 빠름
FKRESTRICT 가능CASCADE 필요
트랜잭션 롤백가능dialect 따라

UPSERT, dialect 차이

같은 키가 있으면 UPDATE, 없으면 INSERT.

-- INSERT ... ON CONFLICT
INSERT INTO users (email, name, login_count)
VALUES ('alice@x.com', 'Alice', 1)
ON CONFLICT (email) DO UPDATE
SET name        = EXCLUDED.name,
  login_count = users.login_count + 1
RETURNING id, login_count;
비고
-- PG 15+ 부터 MERGE 도 지원 (기능 동등은 아님)
-- ON CONFLICT 가 race-condition 측면에서 가장 안전
-- MySQL 의 ON DUPLICATE 는 AUTO_INCREMENT 를 일부 소비 (UPDATE 만 일어나도)

pandas 와 DML 의 대응

SQLpandas
SELECT * FROM tdf
SELECT col FROM tdf[['col']]
SELECT * FROM t WHERE c > 10df[df['c'] > 10] (Pandas Boolean Indexing)
ORDER BY c DESCdf.sort_values('c', ascending=False)
LIMIT 10df.head(10)
DISTINCT cdf['c'].drop_duplicates()
WHERE c IN (...)df[df['c'].isin([...])]
WHERE c LIKE '%x%'df[df['c'].str.contains('x', na=False)]
CASE WHEN ...np.where(...), df['c'].map(...)
INSERTpd.concat([df, new_rows]) 또는 df.loc[len(df)] = ...
UPDATE ... SET c = ... WHERE ...df.loc[mask, 'c'] = ...
DELETE ... WHERE ...df = df[~mask]
WITH cte AS (...)중간 DataFrame 변수

UPDATE/DELETE 가 mask 한 줄로 처리되는 게 pandas 의 강점, 거꾸로 INSERT 의 효율은 SQL 이 압도적. 적재적소.

함정

1. SELECT * 를 운영 코드에

  • 스키마 변경 시 깨짐
  • 네트워크 비용
  • 인덱스 only-scan 못 씀

명시적으로 컬럼 나열.

2. NULL 의 전파

SELECT col_a + col_b FROM t;   -- 어느 한쪽이 NULL 이면 결과 NULL
SELECT COALESCE(col_a, 0) + COALESCE(col_b, 0) FROM t;   -- 0 으로 대체

3. 묵시적 형변환

WHERE id = '1'    -- id 가 INTEGER 면 문자→숫자 변환, 인덱스 쓸 수 있음
WHERE CAST(id AS TEXT) = '1'   -- 컬럼에 함수 적용, 인덱스 깨짐

문자열 칼럼에 숫자를 넣으면 PG 는 에러, MySQL 은 묵시적 변환 (조용한 버그의 원천).

4. WHERE 와 HAVING 혼동

-- ❌ 에러: WHERE 는 집계 결과 못 봄
SELECT city, AVG(salary) FROM users WHERE AVG(salary) > 3000 GROUP BY city;

-- ✓ HAVING 에 써야 함
SELECT city, AVG(salary) FROM users GROUP BY city HAVING AVG(salary) > 3000;

자세한 GROUP BY 의 의미와 동작은 SQL GROUP BY.

5. 페이지 깊은 OFFSET

운영 환경에서 무한 스크롤은 keyset pagination 으로.

관련 위키

이 글의 용어 (12개)
[DB] MySQL / InnoDB: clustered index, redo log, MVCCdatabase-internals
정의 MySQL + InnoDB (기본 스토리지 엔진) 의 clustered index + redo log 기반 행 기반 RDBMS. 웹 / SaaS 의 가장 흔한 backbon…
[DB] PostgreSQL: 프로세스 모델, MVCC, WAL, 확장성database-internals
정의 PostgreSQL 은 오픈소스 ORDBMS. 1986 UC Berkeley POSTGRES 의 후예. MVCC, 확장 가능 타입, JSONB, full-text searc…
[Pandas] Boolean Indexingpandas
정의 Boolean Indexing 은 True/False 의 Series 를 인덱서로 전달 해 행을 선택하는 패턴. pandas 의 가장 흔한 필터링 방법. 기본 <CodeWi…
[Pandas] groupbypandas
정의 는 데이터를 그룹으로 나누고 (split), 각 그룹에 함수를 적용 (apply), 결과를 합쳐 (combine) 새 DataFrame 으로 만드는 split-apply-c…
[Pandas] mergepandas
정의 는 두 DataFrame 을 SQL join 처럼 결합. 4 가지 join 타입 (inner / left / right / outer) 지원. 시각화 4 가지 join 타입…
[Pandas] read_sql / to_sqlpandas
정의 - : SQL 결과 → DataFrame - : DataFrame → DB 테이블 SQLAlchemy 의 connection / engine 객체를 사용. 직접 sqlite…
[SQL] Data Typessql
정의 SQL 의 컬럼은 타입 을 가진다. 가능한 값의 도메인, 가능한 연산, 정렬 순서, 인덱스 종류, 저장 크기가 타입에 따라 결정된다. 같은 의미의 타입도 dialect 마다…
[SQL] DCL TCLsql
정의 - DCL (Data Control Language): 권한. , . - TCL (Transaction Control Language): 트랜잭션 경계. , , , . 같이…
[SQL] DDLsql
정의 DDL (Data Definition Language) 은 스키마를 정의/변경/제거 하는 SQL 의 하위 집합. 대상은 테이블, 인덱스, 뷰, 시퀀스, 스키마, 도메인 등.…
[SQL] GROUP BYsql
정의 는 데이터를 키별로 그룹으로 나누고 각 그룹에 집계 함수 (aggregate) 를 적용해 그룹당 한 행으로 줄이는 연산. SQL 의 split-apply-combine. 와…
[SQL] JOINsql
정의 JOIN 은 두 개 이상의 테이블을 공통 컬럼 (보통 키) 으로 결합해 한 결과셋으로 만드는 연산. SQL 의 관계 모델 핵심. 종류는 5 가지: , , , , . 같은 테…
SQLsql
정의 SQL (Structured Query Language) 은 관계형 데이터베이스 (RDBMS) 와 대화하기 위한 선언형 (declarative) 질의 언어. 1970 년 E…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기