ORM Party

Joins

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"]],
});