SQL 치트시트

PostgreSQL 호환 SQL 핵심 문법과 패턴을 한눈에 정리한 빠른 참조 가이드

🔍

SELECT 쿼리

7 items

SELECT 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)
Tip: OFFSET이 크면 성능이 저하됩니다. 대규모 데이터에는 커서 기반 페이지네이션(WHERE id > last_id)을 사용하세요.

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;
Tip: PostgreSQL의 DISTINCT ON은 각 그룹의 첫 번째 행만 반환합니다. ORDER BY와 함께 사용하세요.

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 items

INNER 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;
Tip: LEFT JOIN + IS NULL 패턴은 "주문이 없는 고객 찾기" 같은 안티 조인에 유용합니다.

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();
Tip: LEFT JOIN에서 WHERE에 오른쪽 테이블 조건을 넣으면 INNER JOIN처럼 동작합니다. ON 절에 넣으세요.
📊

집계

7 items

COUNT, 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;
Tip: WHERE는 집계 전에 행을 필터링하고, HAVING은 집계 후에 그룹을 필터링합니다.

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;
Tip: ROW_NUMBER는 항상 고유한 번호, RANK는 동점 시 같은 순위(다음 순위 건너뜀), DENSE_RANK는 건너뛰지 않습니다.

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 items

INSERT 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;
Tip: UPDATE 전에 항상 WHERE 절을 확인하세요. WHERE 없이 실행하면 모든 행이 수정됩니다.

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;
Tip: EXCLUDED는 삽입하려던 새 값을 참조합니다. 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 items

CREATE 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);
Tip: PostgreSQL 11+에서 DEFAULT가 있는 ADD COLUMN은 테이블 전체를 다시 쓰지 않아 대형 테이블에서도 즉시 완료됩니다.

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);
Tip: CREATE INDEX CONCURRENTLY는 테이블을 잠그지 않아 프로덕션 환경에서 안전합니다. 단, 트랜잭션 내에서는 사용할 수 없습니다.

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 items

Subquery 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
);
Tip: EXISTS는 IN보다 대규모 서브쿼리에서 더 효율적입니다. 옵티마이저가 첫 번째 일치를 찾으면 즉시 중단합니다.

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;
Tip: CTE는 복잡한 쿼리를 단계별로 분리하여 가독성을 크게 향상시킵니다. 디버깅도 용이합니다.

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 items

COALESCE & 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;
Tip: DATE_TRUNC은 월별/주별/일별 집계에 필수적입니다. EXTRACT는 특정 필드만 추출할 때 사용하세요.

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 items

EXPLAIN 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)
Tip: Seq Scan이 대형 테이블에서 나타나면 인덱스 추가를 고려하세요. 단, 소형 테이블에서는 Seq Scan이 더 효율적입니다.

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
Tip: 장시간 실행되는 트랜잭션은 다른 쿼리를 차단할 수 있습니다. 트랜잭션은 가능한 한 짧게 유지하세요.

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로 단계별로 분리하면 가독성과 디버깅이 크게 향상됩니다.

FAQ

Related Tools

Also Used Together