SQLSQL 기초 · 8입문

SQL 실전 패턴 — 앱 개발에서 자주 쓰는 쿼리

SQL실전패턴페이지네이션검색psycopg2prisma

페이지네이션

-- 기본 페이지네이션
SELECT * FROM posts
ORDER BY created_at DESC
LIMIT 20 OFFSET 40;  -- 3페이지 (0-indexed: 페이지2)

-- 커서 기반 (대용량 데이터에 효율적)
-- 이전 페이지 마지막 항목의 id를 커서로 사용
SELECT * FROM posts
WHERE id < 150   -- 커서
ORDER BY id DESC
LIMIT 20;

전체 검색 (Full-Text Search)

-- LIKE 기반 (간단하지만 인덱스 비효율)
SELECT * FROM posts
WHERE title LIKE '%리액트%' OR content LIKE '%리액트%';

-- PostgreSQL 전문 검색 (빠름)
SELECT title, ts_rank(search_vector, query) AS rank
FROM posts, to_tsquery('korean', '리액트') query
WHERE search_vector @@ query
ORDER BY rank DESC;

-- 검색 벡터 컬럼 추가
ALTER TABLE posts ADD COLUMN search_vector TSVECTOR;
UPDATE posts SET search_vector = to_tsvector('english', title || ' ' || content);
CREATE INDEX idx_posts_search ON posts USING GIN(search_vector);

대시보드 집계 쿼리

-- 오늘/이번 주/이번 달 가입자 수
SELECT
    COUNT(*) FILTER (WHERE created_at >= CURRENT_DATE) AS today,
    COUNT(*) FILTER (WHERE created_at >= DATE_TRUNC('week', NOW())) AS this_week,
    COUNT(*) FILTER (WHERE created_at >= DATE_TRUNC('month', NOW())) AS this_month,
    COUNT(*) AS total
FROM users;

-- 일별 매출 추이 (최근 30일)
SELECT
    DATE(created_at) AS date,
    COUNT(*) AS order_count,
    SUM(amount) AS revenue
FROM orders
WHERE created_at >= NOW() - INTERVAL '30 days'
GROUP BY DATE(created_at)
ORDER BY date;

순위와 윈도우 함수

-- 부서별 급여 순위
SELECT
    name,
    department,
    salary,
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank,
    LAG(salary) OVER (PARTITION BY department ORDER BY salary DESC) AS prev_salary,
    salary - LAG(salary) OVER (PARTITION BY department ORDER BY salary DESC) AS diff
FROM employees;

-- 누적 합계 (Running Total)
SELECT
    date,
    revenue,
    SUM(revenue) OVER (ORDER BY date) AS cumulative_revenue
FROM daily_sales;

Python에서 SQL 사용

import psycopg2
from contextlib import contextmanager

DB_CONFIG = {
    "host": "localhost",
    "database": "mydb",
    "user": "postgres",
    "password": "password",
}

@contextmanager
def get_connection():
    conn = psycopg2.connect(**DB_CONFIG)
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()

def get_users(page: int = 1, page_size: int = 20):
    with get_connection() as conn:
        with conn.cursor() as cur:
            # 파라미터화된 쿼리 (SQL 인젝션 방지)
            cur.execute(
                "SELECT id, name, email FROM users ORDER BY id LIMIT %s OFFSET %s",
                (page_size, (page - 1) * page_size)
            )
            columns = [d[0] for d in cur.description]
            return [dict(zip(columns, row)) for row in cur.fetchall()]

Node.js에서 SQL 사용 (Prisma)

// schema.prisma
model User {
    id        Int      @id @default(autoincrement())
    name      String
    email     String   @unique
    posts     Post[]
    createdAt DateTime @default(now())
}

// 사용
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();

// 조회
const users = await prisma.user.findMany({
    where: { name: { contains: "철수" } },
    orderBy: { createdAt: "desc" },
    take: 20,
    skip: 0,
    include: { posts: { take: 3 } },
});

// 생성
const user = await prisma.user.create({
    data: { name: "철수", email: "kim@example.com" },
});

// 원시 SQL
const result = await prisma.$queryRaw`
    SELECT department, AVG(salary) as avg_salary
    FROM employees
    GROUP BY department
`;

SQL 인젝션 방지

# ❌ 위험: 직접 문자열 삽입
query = f"SELECT * FROM users WHERE name = '{user_input}'"
# user_input = "'; DROP TABLE users; --"  → 재앙!

# ✅ 안전: 파라미터화된 쿼리
cur.execute("SELECT * FROM users WHERE name = %s", (user_input,))

SQL 기초 시리즈 정리

주제
1편SQL과 관계형 데이터베이스 개념
2편SELECT, WHERE, ORDER BY, LIMIT
3편JOIN으로 테이블 연결
4편GROUP BY, 집계 함수
5편서브쿼리와 CTE
6편INSERT, UPDATE, DELETE, 트랜잭션
7편DDL, 인덱스, 뷰
8편실전 패턴

다음은 Docker 기초 — 컨테이너로 어디서든 동일한 환경을 구축하는 방법을 배웁니다.

궁금한 점이 있으신가요?

협업·의뢰는 아래로, 가벼운 소통은 인스타그램 @bluefox._.hi도 환영이에요.