An ORM (Object-Relational Mapper) lets you work with your database using your programming language instead of SQL. It reduces boilerplate and prevents SQL injection. But ORMs also add abstraction — and abstraction can hide performance problems.
When to Use an ORM vs Raw SQL
Use an ORM when:
- You do standard CRUD (create, read, update, delete)
- You want type safety and IDE autocomplete
- You want schema migrations integrated with your code
- You are building fast and the query complexity is low
Use raw SQL when:
- You need complex queries (window functions, CTEs, custom aggregations)
- Performance is critical and you need to see exactly what runs
- You need database-specific features the ORM does not expose
- You are debugging a slow query
Most applications use both: ORM for 80% of queries, raw SQL for the complex 20%.
The Same Query in All Three
Fetch users with their recent orders, including order items:
Raw SQL:
SELECT u.id, u.name, u.email,
o.id as order_id, o.total, o.status,
oi.product_id, oi.quantity, oi.unit_price
FROM users u
LEFT JOIN orders o ON o.user_id = u.id AND o.created_at > NOW() - INTERVAL '30 days'
LEFT JOIN order_items oi ON oi.order_id = o.id
WHERE u.id = 42
ORDER BY o.created_at DESC;
Prisma v6 (TypeScript):
const user = await prisma.user.findUnique({
where: { id: 42 },
include: {
orders: {
where: {
createdAt: { gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) },
},
orderBy: { createdAt: "desc" },
include: {
items: true,
},
},
},
});
SQLAlchemy 2.x (Python):
from sqlalchemy.orm import selectinload
from datetime import datetime, timedelta, timezone
user = (
session.query(User)
.options(
selectinload(User.orders).selectinload(Order.items)
)
.filter(
User.id == 42,
Order.created_at >= datetime.now(timezone.utc) - timedelta(days=30)
)
.first()
)
GORM v2 (Go):
var user User
db.Preload("Orders", "created_at > ?", time.Now().AddDate(0, 0, -30)).
Preload("Orders.Items").
First(&user, 42)
Prisma v6
Prisma is a TypeScript ORM with a schema-first workflow. Define your models in schema.prisma, run migrations, and get a fully typed client.
npm install prisma @prisma/client
npx prisma init
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
orders Order[]
createdAt DateTime @default(now())
}
model Order {
id Int @id @default(autoincrement())
userId Int
user User @relation(fields: [userId], references: [id])
total Decimal
status String @default("pending")
items OrderItem[]
createdAt DateTime @default(now())
}
model OrderItem {
id Int @id @default(autoincrement())
orderId Int
order Order @relation(fields: [orderId], references: [id])
productId Int
quantity Int
unitPrice Decimal
}
npx prisma migrate dev --name init
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
// Create
const user = await prisma.user.create({
data: { email: "alex@example.com", name: "Alex" },
});
// Raw SQL escape hatch
const result = await prisma.$queryRaw`
SELECT * FROM users WHERE email ILIKE ${"%" + search + "%"}
`;
SQLAlchemy 2.x
Python’s most popular ORM. Supports both ORM-style and Core (SQL builder) style.
pip install sqlalchemy psycopg2-binary
from sqlalchemy import create_engine, String, Integer, ForeignKey, Numeric, DateTime, text
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session
from datetime import datetime, timezone
engine = create_engine("postgresql+psycopg2://user:pass@localhost/mydb")
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
email: Mapped[str] = mapped_column(String, unique=True)
name: Mapped[str] = mapped_column(String)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
orders: Mapped[list["Order"]] = relationship(back_populates="user")
class Order(Base):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
total: Mapped[float] = mapped_column(Numeric(10, 2))
user: Mapped["User"] = relationship(back_populates="orders")
# Create tables
Base.metadata.create_all(engine)
# Use
with Session(engine) as session:
user = User(email="alex@example.com", name="Alex")
session.add(user)
session.commit()
found = session.query(User).filter(User.email == "alex@example.com").first()
# Raw SQL
result = session.execute(
text("SELECT * FROM users WHERE email ILIKE :q"),
{"q": "%alex%"}
).fetchall()
GORM v2 (Go)
go get gorm.io/gorm gorm.io/driver/postgres
package main
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
"time"
)
type User struct {
gorm.Model
Email string `gorm:"uniqueIndex"`
Name string
Orders []Order
}
type Order struct {
gorm.Model
UserID uint
Total float64
Status string `gorm:"default:pending"`
CreatedAt time.Time
Items []OrderItem
}
type OrderItem struct {
gorm.Model
OrderID uint
ProductID uint
Quantity int
UnitPrice float64
}
func main() {
dsn := "host=localhost user=postgres dbname=mydb sslmode=disable"
db, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{})
db.AutoMigrate(&User{}, &Order{}, &OrderItem{})
// Create
db.Create(&User{Email: "alex@example.com", Name: "Alex"})
// Find with preload
var user User
db.Preload("Orders").First(&user, 1)
// Raw SQL
var users []User
db.Raw("SELECT * FROM users WHERE email LIKE ?", "%alex%").Scan(&users)
}
The N+1 Problem
The N+1 problem is the most common ORM performance issue. Loading 100 users then fetching each user’s orders separately = 101 queries.
// BAD: N+1 — 1 query for users + N queries for orders
const users = await prisma.user.findMany();
for (const user of users) {
const orders = await prisma.order.findMany({ where: { userId: user.id } });
// 1 extra query per user!
}
// GOOD: 1 query with include
const users = await prisma.user.findMany({
include: { orders: true },
});
Always check your ORM’s query log in development to spot N+1 issues.
What’s Next?
You understand ORMs and when to use them. The final article is a complete reference cheat sheet for every database in this series.
Next: Database Tutorial #20: Database Cheat Sheet 2026
Related Articles
- Database Tutorial #18: Database Design Patterns
- Database Tutorial #20: Database Cheat Sheet 2026
- Database Tutorial #3: PostgreSQL — Advanced Queries