SQL 치트시트
PostgreSQL 호환 SQL 핵심 문법과 패턴을 한눈에 정리한 빠른 참조 가이드
SELECT 쿼리
7 itemsSELECT Basics
테이블에서 데이터 조회
-- Select specific columns SELECT name, email, created_at FROM users WHERE active = true; -- Select all columns SELECT * FROM products;
WHERE Conditions
조건을 사용한 데이터 필터링
SELECT * FROM orders WHERE status = 'shipped' AND total > 100 AND created_at >= '2025-01-01'; -- OR condition SELECT * FROM products WHERE category = 'electronics' OR category = 'books';
ORDER BY & LIMIT
결과 정렬 및 개수 제한
-- Sort descending, get top 10 SELECT name, price FROM products ORDER BY price DESC LIMIT 10; -- Pagination with OFFSET SELECT * FROM articles ORDER BY published_at DESC LIMIT 20 OFFSET 40; -- page 3 (0-indexed)
DISTINCT
중복 제거된 고유 값 조회
-- Unique categories SELECT DISTINCT category FROM products; -- Distinct on multiple columns SELECT DISTINCT country, city FROM customers; -- PostgreSQL: DISTINCT ON (first row per group) SELECT DISTINCT ON (user_id) user_id, score, created_at FROM scores ORDER BY user_id, created_at DESC;
BETWEEN, IN, LIKE
범위, 목록, 패턴 매칭 필터
-- Range
SELECT * FROM orders
WHERE total BETWEEN 100 AND 500;
-- List of values
SELECT * FROM users
WHERE role IN ('admin', 'editor', 'moderator');
-- Pattern matching
SELECT * FROM products
WHERE name LIKE '%phone%'; -- contains "phone"
SELECT * FROM users
WHERE email LIKE '%@gmail.com'; -- ends with @gmail.com
SELECT * FROM products
WHERE name ILIKE '%Phone%'; -- case-insensitive (PostgreSQL)IS NULL / IS NOT NULL
NULL 값 검사
-- Find records with missing data SELECT * FROM users WHERE phone IS NULL; -- Find records with data present SELECT * FROM orders WHERE shipped_at IS NOT NULL; -- COALESCE: provide a default for NULL SELECT name, COALESCE(nickname, name) AS display_name FROM users;
Aliases & Expressions
컬럼 별칭과 계산 표현식
SELECT first_name || ' ' || last_name AS full_name, price * quantity AS line_total, ROUND(price * 0.9, 2) AS discounted_price, EXTRACT(YEAR FROM created_at) AS signup_year FROM orders;
Joins
7 itemsINNER JOIN
두 테이블에서 일치하는 행만 결합
SELECT u.name, o.total, o.created_at FROM users u INNER JOIN orders o ON o.user_id = u.id WHERE o.total > 50;
LEFT JOIN
왼쪽 테이블의 모든 행 + 일치하는 오른쪽 행
-- All users, even those without orders SELECT u.name, COUNT(o.id) AS order_count FROM users u LEFT JOIN orders o ON o.user_id = u.id GROUP BY u.id, u.name;
RIGHT JOIN & FULL JOIN
오른쪽 또는 양쪽 테이블의 모든 행 포함
-- RIGHT JOIN: all orders, even without matching users SELECT u.name, o.total FROM users u RIGHT JOIN orders o ON o.user_id = u.id; -- FULL JOIN: all rows from both tables SELECT u.name, o.total FROM users u FULL JOIN orders o ON o.user_id = u.id;
CROSS JOIN
두 테이블의 모든 행 조합 (카르테시안 곱)
-- Generate all size-color combinations SELECT s.size, c.color FROM sizes s CROSS JOIN colors c; -- Equivalent syntax SELECT s.size, c.color FROM sizes s, colors c;
Self Join
같은 테이블을 자기 자신과 조인
-- Find employees and their managers SELECT e.name AS employee, m.name AS manager FROM employees e LEFT JOIN employees m ON e.manager_id = m.id; -- Find users in the same city SELECT a.name, b.name, a.city FROM users a JOIN users b ON a.city = b.city AND a.id < b.id;
Multiple Joins
세 개 이상 테이블 조인
SELECT c.name AS customer, p.name AS product, oi.quantity, o.created_at FROM orders o JOIN customers c ON c.id = o.customer_id JOIN order_items oi ON oi.order_id = o.id JOIN products p ON p.id = oi.product_id WHERE o.created_at >= NOW() - INTERVAL '30 days';
JOIN with Conditions
ON 절에 추가 조건 설정
-- Only join active subscriptions SELECT u.name, s.plan, s.expires_at FROM users u LEFT JOIN subscriptions s ON s.user_id = u.id AND s.status = 'active' AND s.expires_at > NOW();
집계
7 itemsCOUNT, SUM, AVG
기본 집계 함수
SELECT COUNT(*) AS total_orders, COUNT(DISTINCT user_id) AS unique_customers, SUM(total) AS revenue, AVG(total) AS avg_order_value, ROUND(AVG(total), 2) AS avg_rounded FROM orders WHERE created_at >= '2025-01-01';
MIN / MAX
최소값과 최대값 조회
SELECT MIN(price) AS cheapest, MAX(price) AS most_expensive, MAX(created_at) AS latest_order FROM products; -- Per-category min/max SELECT category, MIN(price), MAX(price) FROM products GROUP BY category;
GROUP BY
그룹별 집계
-- Revenue by month
SELECT
DATE_TRUNC('month', created_at) AS month,
COUNT(*) AS orders,
SUM(total) AS revenue
FROM orders
GROUP BY DATE_TRUNC('month', created_at)
ORDER BY month;
-- Multiple group columns
SELECT country, city, COUNT(*) AS user_count
FROM users
GROUP BY country, city
ORDER BY user_count DESC;HAVING
집계 결과에 대한 필터링
-- Categories with more than 10 products SELECT category, COUNT(*) AS product_count FROM products GROUP BY category HAVING COUNT(*) > 10 ORDER BY product_count DESC; -- Customers who spent more than $1000 SELECT user_id, SUM(total) AS total_spent FROM orders GROUP BY user_id HAVING SUM(total) > 1000;
Window Functions — ROW_NUMBER, RANK
윈도우 함수로 그룹 내 순위 매기기
-- Rank products by price within each category SELECT name, category, price, ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC) AS rn, RANK() OVER (PARTITION BY category ORDER BY price DESC) AS rnk, DENSE_RANK() OVER (PARTITION BY category ORDER BY price DESC) AS dense_rnk FROM products; -- Get the top 3 per category SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC) AS rn FROM products ) sub WHERE rn <= 3;
Window Functions — Running Totals
누적 합계와 이동 평균
-- Running total of revenue
SELECT
created_at::date AS day,
total,
SUM(total) OVER (ORDER BY created_at) AS running_total
FROM orders;
-- 7-day moving average
SELECT
day,
revenue,
AVG(revenue) OVER (
ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS moving_avg_7d
FROM daily_revenue;ROLLUP
소계와 총계를 포함한 다단계 집계
SELECT COALESCE(country, 'TOTAL') AS country, COALESCE(city, 'Subtotal') AS city, COUNT(*) AS users, SUM(revenue) AS total_revenue FROM user_revenue GROUP BY ROLLUP(country, city) ORDER BY country, city;
데이터 수정
6 itemsINSERT INTO
새 행 삽입
-- Single row
INSERT INTO users (name, email, role)
VALUES ('Alice', 'alice@example.com', 'admin');
-- Multiple rows
INSERT INTO products (name, price, category) VALUES
('Keyboard', 79.99, 'electronics'),
('Mouse', 29.99, 'electronics'),
('Desk Lamp', 45.00, 'office');UPDATE SET
기존 행 업데이트
-- Update specific rows UPDATE users SET role = 'admin', updated_at = NOW() WHERE email = 'alice@example.com'; -- Update with expression UPDATE products SET price = price * 1.1 WHERE category = 'electronics'; -- Update from another table UPDATE orders o SET status = 'archived' FROM users u WHERE o.user_id = u.id AND u.deleted_at IS NOT NULL;
DELETE FROM
행 삭제
-- Delete specific rows DELETE FROM sessions WHERE expires_at < NOW(); -- Delete with subquery DELETE FROM notifications WHERE user_id IN ( SELECT id FROM users WHERE deleted_at IS NOT NULL );
UPSERT (ON CONFLICT)
삽입 시 충돌이면 업데이트 (PostgreSQL)
-- Insert or update on unique constraint INSERT INTO user_settings (user_id, key, value) VALUES (1, 'theme', 'dark') ON CONFLICT (user_id, key) DO UPDATE SET value = EXCLUDED.value, updated_at = NOW(); -- Insert or do nothing INSERT INTO page_views (user_id, page, viewed_at) VALUES (1, '/home', NOW()) ON CONFLICT DO NOTHING;
RETURNING
수정된 행의 데이터를 즉시 반환 (PostgreSQL)
-- Get the inserted row back
INSERT INTO users (name, email)
VALUES ('Bob', 'bob@example.com')
RETURNING id, name, created_at;
-- Get updated rows
UPDATE orders SET status = 'shipped'
WHERE status = 'processing' AND packed_at < NOW() - INTERVAL '1 day'
RETURNING id, user_id, total;
-- Get deleted rows
DELETE FROM sessions WHERE expires_at < NOW()
RETURNING id, user_id;Bulk Insert from SELECT
SELECT 결과를 다른 테이블에 삽입
-- Copy data between tables INSERT INTO archived_orders (id, user_id, total, created_at) SELECT id, user_id, total, created_at FROM orders WHERE created_at < '2024-01-01'; -- Create summary table INSERT INTO daily_stats (day, orders, revenue) SELECT created_at::date, COUNT(*), SUM(total) FROM orders GROUP BY created_at::date;
테이블 작업
6 itemsCREATE TABLE
새 테이블 생성
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
role VARCHAR(20) DEFAULT 'user',
active BOOLEAN DEFAULT true,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);ALTER TABLE
테이블 구조 변경
-- Add column ALTER TABLE users ADD COLUMN phone VARCHAR(20); -- Add column with default (safe in PostgreSQL 11+) ALTER TABLE users ADD COLUMN is_verified BOOLEAN DEFAULT false; -- Drop column ALTER TABLE users DROP COLUMN phone; -- Rename column ALTER TABLE users RENAME COLUMN name TO full_name; -- Change column type ALTER TABLE products ALTER COLUMN price TYPE NUMERIC(12, 2);
DROP TABLE & TRUNCATE
테이블 삭제 또는 데이터 전체 삭제
-- Drop table (structure + data gone) DROP TABLE IF EXISTS temp_imports; -- Drop with cascade (removes dependent objects) DROP TABLE IF EXISTS orders CASCADE; -- Truncate: delete all rows but keep structure TRUNCATE TABLE logs; -- Truncate with cascade + restart sequences TRUNCATE TABLE orders, order_items RESTART IDENTITY CASCADE;
CREATE INDEX
검색 성능을 위한 인덱스 생성
-- B-tree index (default, most common) CREATE INDEX idx_users_email ON users(email); -- Composite index CREATE INDEX idx_orders_user_date ON orders(user_id, created_at DESC); -- Partial index (only index active users) CREATE INDEX idx_active_users ON users(email) WHERE active = true; -- Unique index CREATE UNIQUE INDEX idx_users_email_unique ON users(email); -- Concurrent (no table lock, production-safe) CREATE INDEX CONCURRENTLY idx_products_name ON products(name);
Constraints
데이터 무결성을 위한 제약 조건
CREATE TABLE order_items (
id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id BIGINT NOT NULL REFERENCES products(id),
quantity INT NOT NULL CHECK (quantity > 0),
price NUMERIC(10, 2) NOT NULL CHECK (price >= 0),
UNIQUE (order_id, product_id)
);
-- Add constraint to existing table
ALTER TABLE users ADD CONSTRAINT chk_role
CHECK (role IN ('admin', 'editor', 'viewer', 'user'));Temporary Tables
세션 범위의 임시 테이블
-- Create temp table (auto-dropped at session end) CREATE TEMP TABLE tmp_import ( raw_name TEXT, raw_email TEXT, processed BOOLEAN DEFAULT false ); -- Create from query CREATE TEMP TABLE active_users AS SELECT id, name, email FROM users WHERE active = true;
서브쿼리 & CTE
6 itemsSubquery in WHERE
WHERE 절에서 서브쿼리 사용
-- Scalar subquery SELECT * FROM products WHERE price > (SELECT AVG(price) FROM products); -- IN subquery SELECT * FROM users WHERE id IN ( SELECT DISTINCT user_id FROM orders WHERE total > 500 );
EXISTS
관련 행의 존재 여부 확인
-- Users who have placed at least one order SELECT u.name, u.email FROM users u WHERE EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.id ); -- Users with NO orders (anti-pattern) SELECT u.name FROM users u WHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.user_id = u.id );
Subquery in FROM
FROM 절에서 파생 테이블로 사용
-- Top spending users SELECT sub.name, sub.total_spent FROM ( SELECT u.name, SUM(o.total) AS total_spent FROM users u JOIN orders o ON o.user_id = u.id GROUP BY u.id, u.name ) sub WHERE sub.total_spent > 1000 ORDER BY sub.total_spent DESC;
CTE (WITH clause)
Common Table Expression으로 쿼리 구조화
WITH monthly_revenue AS (
SELECT
DATE_TRUNC('month', created_at) AS month,
SUM(total) AS revenue
FROM orders
GROUP BY 1
),
monthly_growth AS (
SELECT
month,
revenue,
LAG(revenue) OVER (ORDER BY month) AS prev_revenue,
ROUND(
(revenue - LAG(revenue) OVER (ORDER BY month))
/ LAG(revenue) OVER (ORDER BY month) * 100, 1
) AS growth_pct
FROM monthly_revenue
)
SELECT * FROM monthly_growth ORDER BY month;Recursive CTE
재귀적 쿼리로 트리/계층 구조 조회
-- Organization hierarchy (employee -> manager tree) WITH RECURSIVE org_tree AS ( -- Base case: top-level managers SELECT id, name, manager_id, 1 AS depth FROM employees WHERE manager_id IS NULL UNION ALL -- Recursive case: reports SELECT e.id, e.name, e.manager_id, t.depth + 1 FROM employees e JOIN org_tree t ON t.id = e.manager_id ) SELECT * FROM org_tree ORDER BY depth, name;
Correlated Subquery
외부 쿼리를 참조하는 상관 서브쿼리
-- Each user's latest order SELECT u.name, (SELECT MAX(o.created_at) FROM orders o WHERE o.user_id = u.id) AS last_order FROM users u; -- Products priced above their category average SELECT p.name, p.price, p.category FROM products p WHERE p.price > ( SELECT AVG(p2.price) FROM products p2 WHERE p2.category = p.category );
함수 & 표현식
7 itemsCOALESCE & NULLIF
NULL 값 처리 함수
-- First non-NULL value SELECT COALESCE(nickname, username, 'Anonymous') AS display_name FROM users; -- Avoid division by zero SELECT total / NULLIF(quantity, 0) AS unit_price FROM order_items; -- Default for missing JSON field SELECT COALESCE(metadata->>'theme', 'light') AS theme FROM user_settings;
CASE WHEN
조건부 로직 표현식
SELECT name, price,
CASE
WHEN price >= 100 THEN 'premium'
WHEN price >= 50 THEN 'standard'
ELSE 'budget'
END AS tier
FROM products;
-- In aggregation
SELECT
COUNT(CASE WHEN status = 'active' THEN 1 END) AS active_count,
COUNT(CASE WHEN status = 'inactive' THEN 1 END) AS inactive_count
FROM users;CAST & Type Conversion
데이터 타입 변환
-- Standard SQL CAST
SELECT CAST('42' AS INTEGER);
SELECT CAST(price AS TEXT) FROM products;
-- PostgreSQL shorthand ::
SELECT '2025-01-15'::DATE;
SELECT total::NUMERIC(10,2) FROM orders;
SELECT created_at::DATE AS day FROM orders;
SELECT '{"key":"val"}'::JSONB;String Functions
문자열 처리 함수
SELECT
CONCAT(first_name, ' ', last_name) AS full_name,
UPPER(email) AS email_upper,
LOWER(name) AS name_lower,
LENGTH(description) AS desc_length,
TRIM(' hello ') AS trimmed,
SUBSTRING(phone FROM 1 FOR 3) AS area_code,
REPLACE(url, 'http://', 'https://') AS secure_url,
LEFT(title, 50) || '...' AS truncated,
SPLIT_PART('a.b.c', '.', 2) AS second_part -- 'b'
FROM articles;Date Functions
날짜/시간 처리 함수
SELECT
NOW() AS current_timestamp,
CURRENT_DATE AS today,
DATE_TRUNC('month', NOW()) AS month_start,
EXTRACT(YEAR FROM created_at) AS year,
EXTRACT(DOW FROM created_at) AS day_of_week, -- 0=Sun
created_at + INTERVAL '30 days' AS expires_at,
AGE(NOW(), created_at) AS account_age,
TO_CHAR(created_at, 'YYYY-MM-DD HH24:MI') AS formatted
FROM users;JSON / JSONB Functions
PostgreSQL JSON 데이터 처리
-- Access JSON fields
SELECT
metadata->>'name' AS name, -- text
metadata->'address'->>'city' AS city, -- nested text
metadata->'tags' AS tags_json, -- jsonb
jsonb_array_length(metadata->'tags') AS tag_count
FROM users;
-- Filter by JSON field
SELECT * FROM users
WHERE metadata->>'role' = 'admin';
-- Update JSONB field
UPDATE users
SET metadata = metadata || '{"verified": true}'::jsonb
WHERE id = 1;
-- Build JSON from columns
SELECT jsonb_build_object('id', id, 'name', name) FROM users;Array Functions (PostgreSQL)
PostgreSQL 배열 처리
-- Array contains SELECT * FROM posts WHERE 'react' = ANY(tags); -- Array overlap SELECT * FROM posts WHERE tags && ARRAY['react', 'vue']; -- Unnest array to rows SELECT id, UNNEST(tags) AS tag FROM posts; -- Aggregate to array SELECT user_id, ARRAY_AGG(DISTINCT tag ORDER BY tag) AS all_tags FROM post_tags GROUP BY user_id;
성능 & 팁
7 itemsEXPLAIN ANALYZE
쿼리 실행 계획 분석
-- Show execution plan with actual timing EXPLAIN ANALYZE SELECT u.name, COUNT(o.id) AS orders FROM users u LEFT JOIN orders o ON o.user_id = u.id WHERE u.active = true GROUP BY u.id, u.name; -- Common plan nodes to watch: -- Seq Scan → full table scan (may need index) -- Index Scan → using index (good) -- Hash Join → joining with hash table -- Sort → explicit sorting (check if index can help) -- Nested Loop → for each outer row, scan inner (watch for large sets)
Index Types
PostgreSQL 인덱스 종류와 사용 사례
-- B-tree (default): equality, range, sorting CREATE INDEX idx_date ON orders(created_at); -- GIN: arrays, JSONB, full-text search CREATE INDEX idx_tags ON posts USING GIN(tags); CREATE INDEX idx_meta ON users USING GIN(metadata); -- GiST: geometric, range types, full-text CREATE INDEX idx_location ON places USING GIST(point); -- BRIN: large tables with natural ordering (timestamps) CREATE INDEX idx_log_time ON logs USING BRIN(created_at);
Query Optimization Tips
쿼리 성능 향상 팁
-- 1. Use specific columns instead of SELECT * SELECT id, name, email FROM users; -- not SELECT * -- 2. Use EXISTS instead of COUNT for existence checks -- Bad SELECT CASE WHEN COUNT(*) > 0 THEN true ELSE false END FROM orders WHERE user_id = 1; -- Good SELECT EXISTS(SELECT 1 FROM orders WHERE user_id = 1); -- 3. Use LIMIT for sampling SELECT * FROM logs ORDER BY created_at DESC LIMIT 100; -- 4. Avoid functions on indexed columns in WHERE -- Bad (can't use index) WHERE LOWER(email) = 'alice@example.com' -- Good (use expression index or store lowercase) WHERE email = 'alice@example.com'
Transactions
트랜잭션으로 원자성 보장
BEGIN; -- Debit from one account UPDATE accounts SET balance = balance - 100 WHERE id = 1 AND balance >= 100; -- Credit to another account UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- Verify SELECT id, balance FROM accounts WHERE id IN (1, 2); COMMIT; -- or ROLLBACK if something went wrong -- Savepoints for partial rollback BEGIN; SAVEPOINT sp1; INSERT INTO orders (...) VALUES (...); SAVEPOINT sp2; INSERT INTO order_items (...) VALUES (...); -- fails ROLLBACK TO sp2; -- undo only order_items COMMIT; -- order still committed
VACUUM & Maintenance
테이블 정리 및 통계 업데이트
-- Update statistics for query planner ANALYZE users; -- Reclaim dead tuple space VACUUM users; -- Full vacuum (exclusive lock, reclaims disk space) VACUUM FULL large_table; -- Check table bloat SELECT relname AS table, n_dead_tup AS dead_tuples, n_live_tup AS live_tuples, last_autovacuum FROM pg_stat_user_tables ORDER BY n_dead_tup DESC;
Table Partitioning
대규모 테이블을 파티션으로 분할
-- Create partitioned table (PostgreSQL 10+)
CREATE TABLE events (
id BIGSERIAL,
event_type TEXT NOT NULL,
payload JSONB,
created_at TIMESTAMPTZ NOT NULL
) PARTITION BY RANGE (created_at);
-- Create monthly partitions
CREATE TABLE events_2025_01 PARTITION OF events
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE events_2025_02 PARTITION OF events
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
-- Query automatically prunes irrelevant partitions
SELECT * FROM events
WHERE created_at >= '2025-01-15' AND created_at < '2025-02-01';Useful System Queries
데이터베이스 모니터링용 시스템 쿼리
-- Table sizes SELECT relname AS table, pg_size_pretty(pg_total_relation_size(relid)) AS total_size FROM pg_catalog.pg_statio_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 10; -- Active queries SELECT pid, state, query, NOW() - query_start AS duration FROM pg_stat_activity WHERE state = 'active' AND pid <> pg_backend_pid(); -- Index usage SELECT relname AS table, indexrelname AS index, idx_scan AS scans, idx_tup_read AS rows_read FROM pg_stat_user_indexes ORDER BY idx_scan DESC LIMIT 10;
SQL 치트시트 사용 가이드
SQL(Structured Query Language)은 관계형 데이터베이스에서 데이터를 조회, 수정, 관리하기 위한 표준 언어입니다. 이 치트시트는 PostgreSQL 호환 문법을 중심으로 일상적으로 사용하는 핵심 패턴을 정리했습니다.
이 치트시트의 활용법
섹션별로 독립적으로 참조할 수 있습니다. 검색창을 통해 필요한 SQL 구문을 빠르게 찾고, 코드 블록을 복사하여 바로 사용하세요. PostgreSQL 특화 기능은 별도로 표시되어 있습니다.
PostgreSQL의 장점
PostgreSQL은 JSONB, 배열, 윈도우 함수, CTE, 파티셔닝 등 고급 기능을 기본 제공하며, ACID 트랜잭션과 MVCC를 통해 동시성과 데이터 무결성을 보장합니다. 오픈소스이면서 엔터프라이즈급 성능을 제공합니다.
쿼리 작성 모범 사례
SELECT *를 피하고 필요한 컬럼만 명시하세요. WHERE 절에 인덱싱된 컬럼을 활용하고, JOIN 조건을 명확히 하세요. 복잡한 쿼리는 CTE로 단계별로 분리하면 가독성과 디버깅이 크게 향상됩니다.