ORM = 파이썬으로 DB 조작 (편리하고 안전)
SQL = DB에게 직접 명령 (강력하고 세밀한 제어 가능)둘 중 하나만 쓰는 게 아니라, ORM 기본 + 복잡한 쿼리는 SQL 조합이 실무 정석이에요! 🎯
파이썬 클래스 ←→ DB 테이블
파이썬 객체 ←→ DB 행 (Row)
클래스 속성 ←→ DB 컬럼 (Column)
User 클래스 ←→ users 테이블
user = User() ←→ users 테이블의 한 행
user.name ←→ name 컬럼의 값
## 언제 뭘 쓸까?
ORM 쓸 때
├── 일반적인 CRUD 작업
├── 빠른 개발이 필요할 때
├── DB를 바꿀 가능성이 있을 때
└── 보안이 중요할 때
SQL 직접 쓸 때
├── 복잡한 통계/집계 쿼리
├── 성능이 극도로 중요할 때
└── ORM으로 표현이 너무 복잡할 때
| | SQL | ORM |
| ------ | ------------ | ----------------- |
| 언어 | SQL 문법 | 클래스/메서드로 작성 |
| 작성 방식 | 문자열로 직접 작성 | 클래스/메서드로 작성 |
| DB 종속성 | DB 바꾸면 문법 바뀜 | 코드 그대로 유지 |
| 러닝커브 | SQL 별도 학습 필요 | 파이썬만 알면 됨 |
| 복잡한 쿼리 | 자유롭고 강력함 | 한계가 있음 |
| 성능 제어 | 직접 최적화 가능 | ORM이 알아서 (N+1 위험) |
| 보안 | SQL 인젝션 위험 | 자동 방어 |
---
## 같은 작업, 다른 문법
### 조회
```python
# SQL
SELECT * FROM question WHERE id = 1;
# ORM
Question.objects.get(id=1)
| 장점 | 단점 | |
|---|---|---|
| 생산성 | SQL 몰라도 DB 사용 가능 | 복잡한 쿼리는 오히려 어려움 |
| 안전성| | 인젝션 자동 방어 | 잘못 쓰면 N+1 성능 문제 |
| 유지보수 | 코드가 직관적 | 내부 SQL 파악이 어려울 수 있음 |
| 이식성 | DB 바꿔도 코드 그대로 | ORM마다 문법이 달라 |
# SQL
SELECT * FROM question WHERE subject LIKE '%파이썬%' ORDER BY create_date DESC;
# ORM
Question.objects.filter(subject__contains='파이썬').order_by('-create_date')
# SQL
INSERT INTO question (subject, content) VALUES ('제목', '내용');
# ORM
Question.objects.create(subject='제목', content='내용')
# SQL
SELECT q.subject, a.content
FROM question q
JOIN answer a ON q.id = a.question_id
WHERE q.id = 1;
# ORM
question = Question.objects.prefetch_related('answer_set').get(id=1)
question.answer_set.all()
# SQL — MySQL에서 PostgreSQL로 바꾸면
# MySQL
SELECT * FROM question LIMIT 10;
# PostgreSQL
SELECT * FROM question LIMIT 10 OFFSET 0; # 문법이 달라짐 😰
# ORM — DB 바꿔도 코드 동일
Question.objects.all()[:10] # 어떤 DB든 그대로 ✅
# SQL — 위험! 😰
user_input = "1 OR 1=1" # 악의적인 입력
query = f"SELECT * FROM users WHERE id = {user_input}"
# → SELECT * FROM users WHERE id = 1 OR 1=1
# → 전체 데이터가 털림! 💀
# ORM — 자동으로 막아줌 ✅
User.objects.get(id=user_input)
# 내부적으로 자동 이스케이프 처리
# 복잡한 통계 쿼리 — SQL이 훨씬 직관적
# SQL
SELECT DATE(create_date), COUNT(*) as cnt
FROM question
GROUP BY DATE(create_date)
HAVING cnt >= 10
ORDER BY cnt DESC;
# ORM — 복잡하고 가독성 떨어짐 😰
from django.db.models import Count
from django.db.models.functions import TruncDate
Question.objects \
.annotate(date=TruncDate('create_date')) \
.values('date') \
.annotate(cnt=Count('id')) \
.filter(cnt__gte=10) \
.order_by('-cnt')
ORM 쓸 때
├── 일반적인 CRUD 작업
├── 빠른 개발이 필요할 때
├── DB를 바꿀 가능성이 있을 때
└── 보안이 중요할 때
SQL 직접 쓸 때
├── 복잡한 통계/집계 쿼리
├── 성능이 극도로 중요할 때
└── ORM으로 표현이 너무 복잡할 때
# Django에서 복잡한 건 Raw SQL 직접 사용
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("""
SELECT DATE(create_date), COUNT(*)
FROM question
GROUP BY DATE(create_date)
""")
result = cursor.fetchall()
ORM = 파이썬으로 DB 조작 (편리하고 안전) SQL = DB에게 직접 명령 (강력하고 세밀한 제어 가능)
둘 중 하나만 쓰는 게 아니라, ORM 기본 + 복잡한 쿼리는 SQL 조합이 실무 정석이에요! 🎯
ORM이 없으면 파이썬에서 DB 쓸 때 이렇게 해야 해요:
# ORM 없이 (Raw SQL)
import sqlite3
conn = sqlite3.connect('db.sqlite3')
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE age >= 25")
rows = cursor.fetchall()
for row in rows:
print(row[0], row[1]) # 인덱스로 접근 😰
ORM 쓰면:
# ORM 있으면 (Django ORM)
users = User.objects.filter(age__gte=25)
for user in users:
print(user.id, user.name) # 속성으로 접근 😊
SQL을 직접 안 써도 파이썬 코드로 DB를 조작할 수 있게 해주는 것!
파이썬 클래스 ←→ DB 테이블
파이썬 객체 ←→ DB 행 (Row)
클래스 속성 ←→ DB 컬럼 (Column)
User 클래스 ←→ users 테이블
user = User() ←→ users 테이블의 한 행
user.name ←→ name 컬럼의 값
# Django ORM
from django.db import models
class User(models.Model):
name = models.CharField(max_length=100) # VARCHAR(100)
age = models.IntegerField() # INT
email = models.EmailField(unique=True) # VARCHAR UNIQUE
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
# 이게 내부적으로 이 SQL로 변환됨 ↓
# CREATE TABLE users (
# id INT PRIMARY KEY AUTO_INCREMENT,
# name VARCHAR(100),
# age INT,
# email VARCHAR(200) UNIQUE,
# created_at DATETIME
# );
# SQL
INSERT INTO users (name, age, email) VALUES ('김철수', 25, 'chul@gmail.com');
# Django ORM
user = User(name='김철수', age=25, email='chul@gmail.com')
user.save()
# 더 짧게
User.objects.create(name='김철수', age=25, email='chul@gmail.com')
# SQL
SELECT * FROM users;
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE age >= 25 ORDER BY name;
# Django ORM
User.objects.all() # 전체 조회
User.objects.get(id=1) # 단건 조회
User.objects.filter(age__gte=25).order_by('name') # 조건 + 정렬
# SQL
UPDATE users SET age = 26 WHERE id = 1;
# Django ORM
user = User.objects.get(id=1)
user.age = 26
user.save()
# 한 줄로
User.objects.filter(id=1).update(age=26)
# SQL
DELETE FROM users WHERE id = 1;
# Django ORM
User.objects.filter(id=1).delete()
# SQL WHERE 조건들을 ORM으로
User.objects.filter(age=25) # age = 25
User.objects.filter(age__gte=25) # age >= 25
User.objects.filter(age__lte=25) # age <= 25
User.objects.filter(age__gt=25) # age > 25
User.objects.filter(age__lt=25) # age < 25
User.objects.filter(age__in=[20,25]) # age IN (20, 25)
User.objects.filter(name__contains='철') # LIKE '%철%'
User.objects.filter(name__startswith='김') # LIKE '김%'
User.objects.filter(name__isnull=True) # IS NULL
# 범위
User.objects.filter(age__range=(20, 30)) # BETWEEN 20 AND 30
# AND 조건
User.objects.filter(age__gte=20, name__startswith='김')
# OR 조건
from django.db.models import Q
User.objects.filter(Q(age=25) | Q(name='이영희'))
# 테이블 관계 정의
class User(models.Model):
name = models.CharField(max_length=100)
class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) # 1:N
title = models.CharField(max_length=200)
content = models.TextField()
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE) # 1:1
bio = models.TextField()
class Tag(models.Model):
posts = models.ManyToManyField(Post) # N:M
name = models.CharField(max_length=50)
# JOIN 쿼리
# SQL
SELECT * FROM posts JOIN users ON posts.user_id = users.id WHERE users.name = '김철수';
# ORM (훨씬 직관적!)
posts = Post.objects.filter(user__name='김철수'<)
# 역방향 접근 (유저의 모든 게시글)
user = User.objects.get(id=1)
user.post_set.all(<) # 자동으로 생성되는 역참조
ORM의 대표적인 함정이에요:
# ❌ 나쁜 예 (N+1 문제)
# 유저 10명 조회 → 쿼리 1번
# 각 유저의 게시글 조회 → 쿼리 10번
# 총 11번 DB 조회 발생!
users = User.objects.all() # 쿼리 1번
for user in users:
print(user.post_set.all()) # 유저마다 쿼리 1번씩 추가 💀
# ✅ 좋은 예 (select_related / prefetch_related)
# 한 번에 JOIN해서 가져옴 → 쿼리 1번으로 끝!
users = User.objects.prefetch_related('post_set').all()
for user in users:
print(user.post_set.all()) # 추가 쿼리 없음 ✅
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(100))
age = Column(Integer)
email = Column(String(200), unique=True)
# 조회
session.query(User).filter(User.age >= 25).all()
session.query(User).filter(User.name == '김철수').first()
# 생성
user = User(name='김철수', age=25)
session.add(user)
session.commit()
| 장점 | |단점 | |
|---|---|---|
| 생산성 | SQL 몰라도 DB 사용 가능 | 복잡한 쿼리는 오히려 어려움 |
| 안전성| | 인젝션 자동 방어 | 잘못 쓰면 N+1 성능 문제 |
| 유지보수 | 코드가 직관적 | 내부 SQL 파악이 어려울 수 있음 |
| 이식성 | DB 바꿔도 코드 그대로 | ORM마다 문법이 달라 |
SQL = DB에게 직접 말 걸기
ORM = 파이썬이 통역사 역할을 해서 대신 SQL로 변환해주는 것
User.objects.filter(age__gte=25)
↓ ORM이 자동 변환
SELECT * FROM users WHERE age >= 25;
파이썬 코드 → ORM → SQL → DB 순서로 동작하고, 개발자는 SQL 대신 파이썬 문법만으로 DB를 다룰 수 있어요! 🎯