DISTINCT + DISTINCT ON
Dedupe row sets. .distinct() is universal; .distinctOn() picks one row per group.
.distinct() dedups the result row set. .distinctOn([cols]) picks
ONE row per unique value of the listed columns — the "latest per
channel" / "best per user" pattern.
.distinct() — basic dedup
await ctx.store.query(
queryFor(database.messages)
.select('userId')
.distinct()
.descriptor,
)
// → distinct user IDs that have any messageCross-dialect: standard SQL, supported on every dialect we ship.
.distinctOn([cols]) — pick the first per group
// Latest message per channel
await ctx.store.query(
queryFor(database.messages)
.distinctOn(['channelId'])
.orderBy('channelId', 'asc')
.orderBy('createdAt', 'desc')
.descriptor,
)The order matters: postgres picks the FIRST row per
distinctOn-column combination AS DETERMINED BY THE FULL orderBy.
Always set:
- The
distinctOncolumns first inorderBy - The tie-breaker column second (which row to pick when multiple match — typically a timestamp or id)
Without that order, postgres still picks one row but the choice is unstable.
Cross-dialect
- Postgres — native
DISTINCT ONclause. - MySQL / MariaDB / MSSQL / SQLite — no native
DISTINCT ON. The framework falls back to plainDISTINCTand logs no warning because the fallback covers most use cases. If you specifically need one-row-per-group semantics on these dialects, use a window function withROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)and filter torank = 1in a sub-query.
When NOT to use .distinct()
Distinct on a row set with multiple non-unique columns is rarely
what you want — it dedups by the full row shape, including columns
you might not have intended to constrain. If you want one row per
some-key, use .distinctOn([key]) (postgres) or groupBy([key])
with aggregate({}) to be explicit.
See also
- Aggregations —
.groupBy()+ window functions for cases distinct can't express - Sub-queries —
notInSubqueryfor "rows not appearing elsewhere"