Sub-query dengan .as() untuk nested queries di JOIN, Common Table Expressions (CTE) dengan $with() untuk reusable queries, kapan menggunakan sub-query vs CTE berdasarkan kompleksitas, serta performance considerations termasuk index strategy untuk kolom JOIN dan WHERE.

Setelah di episode 8 kita mempelajari SQL-like JOIN Queries — inner join, left join, multi-table join — pada episode ini kita masuk ke sub-query dan CTE: teknik untuk memecah query kompleks menjadi bagian-bagian yang lebih mudah dikelola dan di-read.
Sub-query dan CTE adalah tools yang powerful untuk queries analitik dan reporting. Memahami kapan menggunakan masing-masing akan meningkatkan kualitas query kalian.
Sub-query adalah query di dalam query — digunakan sebagai tabel atau expression di FROM, WHERE, atau JOIN.
const postCounts = db
.select({
userId: posts.authorId,
totalPosts: count(),
})
.from(posts)
.groupBy(posts.authorId)
.as('post_counts')
const result = await db
.select({
userName: users.name,
totalPosts: postCounts.totalPosts,
})
.from(users)
.innerJoin(postCounts, eq(users.id, postCounts.userId)).as('post_counts') menamai sub-query sehingga bisa dirujuk di JOIN.
const activeUserIds = db
.select({ id: users.id })
.from(users)
.where(eq(users.isActive, true))
.as('active_users')
await db.select().from(posts).where(
inArray(posts.authorId, db.select({ id: activeUserIds.id }).from(activeUserIds))
)CTE adalah named temporary result set yang bisa digunakan beberapa kali dalam satu query. Lebih readable dibanding sub-query untuk queries kompleks.
const activeUsers = db.$with('active_users').as(
db.select().from(users).where(eq(users.isActive, true))
)
const result = await db
.with(activeUsers)
.select()
.from(activeUsers)const postStats = db.$with('post_stats').as(
db.select({
authorId: posts.authorId,
totalPosts: count(),
avgViews: avg(posts.viewCount),
})
.from(posts)
.groupBy(posts.authorId)
)
const result = await db
.with(postStats)
.select({
userName: users.name,
totalPosts: postStats.totalPosts,
avgViews: postStats.avgViews,
})
.from(users)
.innerJoin(postStats, eq(users.id, postStats.authorId))
.where(gte(postStats.totalPosts, 5))| Aspek | Sub-Query | CTE |
|---|---|---|
| Readability | Sulit untuk queries panjang | Sangat readable |
| Reusable | Tidak (inline) | Ya (bisa digunakan berkali-kali) |
| Complexity | Cocok untuk sekali pakai | Cocok untuk multi-step |
| Performance | Sama (optimizedName oleh DB) | Sama |
Tip
Gunakan sub-query untuk sekali pakai (one-off). Gunakan CTE untuk queries kompleks yang butuh readability atau reusable. Keduanya di-optimasi oleh database engine — tidak ada perbedaan performance yang signifikan.
Pastikan kolom yang digunakan di JOIN dan WHERE memiliki index:
import { index } from 'drizzle-orm/pg-core'
export const posts = pgTable('posts', {
// ... columns
}, (table) => ({
authorIdIdx: index('posts_author_id_idx').on(table.authorId),
statusIdx: index('posts_status_idx').on(table.status),
}))Tanpa index, query yang menggunakan JOIN dan WHERE akan melakukan full table scan — sangat lambat untuk dataset besar.
Relational queries Drizzle sudah mengoptimasi untuk menghindari N+1 problem. Tidak perlu khawatir tentang lazy loading seperti di TypeORM.
Pada episode 9 ini, kalian telah memahami Sub-Query & CTE:
.as() untuk nested queries di JOIN dan WHERE.$with() untuk reusable, readable queries.Di episode 10 selanjutnya kita akan mempelajari Drizzle Kit: Generate & Run Migrations — workflow migrasi dari schema changes hingga migration files, push vs migrate, dan best practice. Sampai jumpa di episode 10!