Select ORMs to compare
Pick one or more ORMs from the bar above
Inner Join
SQL
SELECT u.name, p.title
FROM users u
INNER JOIN posts p ON p.author_id = u.id;
Django ORM
# Django uses implicit joins via lookups
posts = Post.objects.select_related("author").values(
"author__name", "title"
)
SQLAlchemy
from sqlalchemy import select
with Session(engine) as session:
rows = session.execute(
select(User.name, Post.title)
.join(Post, User.id == Post.author_id)
).all()
SQLModel
from sqlmodel import select
with Session(engine) as session:
# SQLModel uses explicit join expressions
posts = session.exec(
select(User.name, Post.title)
.join(Post, User.id == Post.author_id)
).all()
Tortoise ORM
# Tortoise uses implicit joins via lookups
posts = await Post.all().select_related(
"author"
).values("author__name", "title")
Peewee
# Peewee uses explicit join expressions
posts = (
Post.select(User.name.alias("author_name"), Post.title)
.join(User, on=(Post.author == User.id))
)
Prisma
// Prisma doesn't expose raw joins;
// use include to fetch related data
const posts = await prisma.post.findMany({
include: { author: true },
});
// Access joined data
posts.forEach((p) => console.log(p.author.name, p.title));
Drizzle
const result = await db
.select({ name: users.name, title: posts.title })
.from(users)
.innerJoin(posts, eq(posts.authorId, users.id));
Kysely
const posts = await db
.selectFrom('posts as p')
.innerJoin('users as u', 'u.id', 'p.author_id')
.select(['u.name as author_name', 'p.title'])
.execute()
TypeORM
const result = await userRepository
.createQueryBuilder("user")
.innerJoinAndSelect("user.posts", "post")
.select(["user.name", "post.title"])
.getRawMany();
MikroORM
// MikroORM uses explicit joins via QueryBuilder
const posts = await orm.em
.createQueryBuilder(Post, "p")
.select(["a.name as author_name", "p.title"])
.join("p.author", "a")
.execute();
Sequelize
// Sequelize uses include + required for INNER JOIN
const posts = await Post.findAll({
attributes: ["title"],
include: [
{
model: User,
as: "author",
attributes: ["name"],
required: true,
},
],
});Left Join
SQL
-- All users, even those without posts
SELECT u.name, p.title
FROM users u
LEFT JOIN posts p ON p.author_id = u.id;
Django ORM
# prefetch_related uses a separate query (like LEFT JOIN behavior)
users = User.objects.prefetch_related("posts").all()
for user in users:
posts = user.posts.all() # empty list if no posts
SQLAlchemy
from sqlalchemy import select
with Session(engine) as session:
rows = session.execute(
select(User.name, Post.title)
.outerjoin(Post, User.id == Post.author_id)
).all()
SQLModel
from sqlmodel import select
with Session(engine) as session:
# Outer join keeps users with no posts
rows = session.exec(
select(User.name, Post.title)
.outerjoin(Post, User.id == Post.author_id)
).all()
Tortoise ORM
# prefetch_related uses separate query (LEFT JOIN behavior)
users = await User.all().prefetch_related("posts")
for user in users:
posts = user.posts # empty list if no posts
Peewee
from peewee import JOIN
# LEFT JOIN includes users with no posts
users = (
User.select(User, Post)
.join(Post, JOIN.LEFT_OUTER, on=(Post.author == User.id))
)
Prisma
// include acts like a left join — returns users even without posts
const users = await prisma.user.findMany({
include: { posts: true },
});
// users without posts have posts: []
Drizzle
const result = await db
.select({ name: users.name, title: posts.title })
.from(users)
.leftJoin(posts, eq(posts.authorId, users.id));
Kysely
const users = await db
.selectFrom('users as u')
.leftJoin('posts as p', 'p.author_id', 'u.id')
.select(['u.name', 'p.title'])
.execute()
TypeORM
const result = await userRepository
.createQueryBuilder("user")
.leftJoinAndSelect("user.posts", "post")
.getMany();
// users without posts have posts: []
MikroORM
// populate uses separate query (LEFT JOIN behavior)
const users = await orm.em.find(User, {}, {
populate: ["posts"],
});
for (const user of users) {
const posts = user.posts; // empty collection if no posts
}
Sequelize
// include with required=false behaves like LEFT JOIN
const users = await User.findAll({
include: [{ model: Post, as: "posts", required: false }],
});
for (const user of users) {
const posts = user.posts ?? []; // empty array if no posts
}Nested Relations (Deep Eager Load)
SQL
-- Posts with author and tags (two joins)
SELECT p.title, u.name AS author, t.name AS tag
FROM posts p
JOIN users u ON u.id = p.author_id
LEFT JOIN post_tags pt ON pt.post_id = p.id
LEFT JOIN tags t ON t.id = pt.tag_id;
Django ORM
# Fetch posts with author and tags
posts = Post.objects.select_related("author").prefetch_related("tags").all()
for post in posts:
print(post.author.name, [t.name for t in post.tags.all()])
SQLAlchemy
from sqlalchemy.orm import selectinload, joinedload
from sqlalchemy import select
with Session(engine) as session:
posts = session.scalars(
select(Post)
.options(
joinedload(Post.author),
selectinload(Post.tags),
)
).unique().all()
SQLModel
from sqlalchemy.orm import joinedload, selectinload
from sqlmodel import select
with Session(engine) as session:
# Fetch posts with author and tags
posts = session.exec(
select(Post).options(
joinedload(Post.author),
selectinload(Post.tags),
)
).all()
for post in posts:
print(post.author.name, [t.name for t in post.tags])
Tortoise ORM
# Fetch posts with author and tags
posts = await Post.all().select_related(
"author"
).prefetch_related("tags")
for post in posts:
print(post.author.name, [t.name for t in post.tags])
Peewee
from peewee import JOIN
# Fetch posts with author and tags
rows = (
Post.select(Post, User, Tag)
.join(User, on=(Post.author == User.id))
.switch(Post)
.join(PostTag, JOIN.LEFT_OUTER, on=(PostTag.post == Post.id))
.join(Tag, JOIN.LEFT_OUTER, on=(PostTag.tag == Tag.id))
)
Prisma
const posts = await prisma.post.findMany({
include: {
author: true,
tags: true,
},
});
Drizzle
const result = await db.query.posts.findMany({
with: {
author: true,
tags: true,
},
});
Kysely
const rows = await db
.selectFrom('posts as p')
.innerJoin('users as u', 'u.id', 'p.author_id')
.leftJoin('post_tags as pt', 'pt.post_id', 'p.id')
.leftJoin('tags as t', 't.id', 'pt.tag_id')
.select([
'p.id as post_id',
'p.title',
'u.name as author_name',
't.name as tag_name',
])
.execute()
TypeORM
const posts = await postRepository.find({
relations: {
author: true,
tags: true,
},
});
MikroORM
// Fetch posts with author and tags
const posts = await orm.em.find(Post, {}, {
populate: ["author", "tags"],
});
for (const post of posts) {
console.log(
post.author.name,
post.tags.getItems().map((t) => t.name),
);
}
Sequelize
// Fetch posts with author and tags
const posts = await Post.findAll({
include: [
{ model: User, as: "author" },
{ model: Tag, as: "tags" },
],
});
for (const post of posts) {
console.log(post.author?.name, post.tags?.map((t) => t.name));
}Aggregate (GROUP BY)
SQL
-- Count posts per author
SELECT u.name, COUNT(p.id) AS post_count
FROM users u
LEFT JOIN posts p ON p.author_id = u.id
GROUP BY u.id, u.name
ORDER BY post_count DESC;
Django ORM
from django.db.models import Count
users = (
User.objects
.annotate(post_count=Count("posts"))
.order_by("-post_count")
)
for user in users:
print(user.name, user.post_count)
SQLAlchemy
from sqlalchemy import select, func
with Session(engine) as session:
rows = session.execute(
select(User.name, func.count(Post.id).label("post_count"))
.outerjoin(Post, User.id == Post.author_id)
.group_by(User.id, User.name)
.order_by(func.count(Post.id).desc())
).all()
SQLModel
from sqlalchemy import func
from sqlmodel import select
with Session(engine) as session:
users = session.exec(
select(User.name, func.count(Post.id).label("post_count"))
.outerjoin(Post, User.id == Post.author_id)
.group_by(User.id, User.name)
.order_by(func.count(Post.id).desc())
).all()
Tortoise ORM
from tortoise.functions import Count
users = await User.annotate(
post_count=Count("posts")
).order_by("-post_count").values("name", "post_count")
Peewee
from peewee import JOIN, fn
users = (
User.select(
User.name,
fn.COUNT(Post.id).alias("post_count"),
)
.join(Post, JOIN.LEFT_OUTER, on=(Post.author == User.id))
.group_by(User.id, User.name)
.order_by(fn.COUNT(Post.id).desc())
)
Prisma
const postCounts = await prisma.post.groupBy({
by: ["authorId"],
_count: { id: true },
orderBy: { _count: { id: "desc" } },
});
// Fetch user names for the results
const users = await prisma.user.findMany({
where: { id: { in: postCounts.map((p) => p.authorId) } },
});
Drizzle
const result = await db
.select({
name: users.name,
postCount: count(posts.id),
})
.from(users)
.leftJoin(posts, eq(posts.authorId, users.id))
.groupBy(users.id, users.name)
.orderBy(desc(count(posts.id)));
Kysely
import { sql } from 'kysely'
const users = await db
.selectFrom('users as u')
.leftJoin('posts as p', 'p.author_id', 'u.id')
.select([
'u.name',
sql<number>`count(p.id)`.as('post_count'),
])
.groupBy(['u.id', 'u.name'])
.orderBy('post_count', 'desc')
.execute()
TypeORM
const result = await userRepository
.createQueryBuilder("user")
.leftJoin("user.posts", "post")
.select("user.name", "name")
.addSelect("COUNT(post.id)", "postCount")
.groupBy("user.id")
.addGroupBy("user.name")
.orderBy("postCount", "DESC")
.getRawMany();
MikroORM
const users = await orm.em
.createQueryBuilder(User, "u")
.select(["u.name", "count(p.id) as post_count"])
.leftJoin("u.posts", "p")
.groupBy(["u.id", "u.name"])
.orderBy({ post_count: "desc" })
.execute();
Sequelize
import { col, fn } from "sequelize";
const users = await User.findAll({
attributes: [
"name",
[fn("COUNT", col("posts.id")), "postCount"],
],
include: [{ model: Post, as: "posts", attributes: [], required: false }],
group: ["User.id", "User.name"],
order: [[fn("COUNT", col("posts.id")), "DESC"]],
});