Select the First Row per Group in Supabase Postgres
The "greatest-N-per-group" problem: one representative row per group. Postgres solves it with DISTINCT ON; supabase-js can't express that directly, so you wrap it in a view or an RPC function. Both confirmed by Supabase maintainers.
You want the most recent order per customer, or the highest-scoring row per team. You reach for GROUP BY... and hit a wall: GROUP BY returns aggregates, not the full non-aggregated row that achieved the max. This is the classic greatest-N-per-group problem, and PostgreSQL has a clean answer that supabase-js can't express directly.
The idiomatic way: DISTINCT ON#
PostgreSQL's DISTINCT ON "keeps only the first row of each set of rows where the given expressions evaluate to equal." The most-recent-row-per-group query:
SELECT DISTINCT ON (customer_id) customer_id, id, total, created_at
FROM orders
ORDER BY customer_id, created_at DESC;The ORDER BY must lead with the same column(s) as DISTINCT ON, then add the tiebreaker that decides the winner. From the Postgres docs: "The DISTINCT ON expression(s) must match the leftmost ORDER BY expression(s)" and "the 'first row' of each set is unpredictable unless ORDER BY is used."
So ORDER BY customer_id, created_at DESC = "group by customer, and within each group the newest order wins." Drop the created_at DESC and the result becomes non-deterministic.
The portable way: ROW_NUMBER() in a subquery#
If you want top-N (not just top-1), or you prefer SQL that ports to other databases, use a window function:
SELECT customer_id, id, total
FROM (
SELECT customer_id, id, total,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY created_at DESC) AS rn
FROM orders
) ranked
WHERE rn = 1; -- change to <= 3 for the three latest per customerYou must wrap it in a subquery. The Postgres docs are explicit: window functions "are permitted only in the SELECT list and the ORDER BY clause... They are forbidden... in WHERE clauses, [because] they logically execute after the processing of those clauses." Add a secondary column to the window's ORDER BY to break ties deterministically.
The often-fastest way: LATERAL#
When you have a small driving set of groups and a good index, a LATERAL join does an index top-1 lookup per group instead of scanning and sorting everything:
SELECT c.id AS customer, top.*
FROM customers c
CROSS JOIN LATERAL (
SELECT o.id, o.total
FROM orders o
WHERE o.customer_id = c.id
ORDER BY o.created_at DESC
LIMIT 1
) AS top;Use LEFT JOIN LATERAL ... ON true if you want customers with zero orders to still appear.
Which approach wins depends on your data and indexes — there is no universal answer. The single most useful index for all three is a composite on (group_col, tiebreaker DESC). Confirm the plan with EXPLAIN ANALYZE rather than trusting a blog (including this one).
Running it from Supabase#
Here's the catch: the supabase-js query builder cannot issue DISTINCT ON (nor SELECT DISTINCT). In the official Supabase discussion, maintainers say it directly — "You can create a view to achieve it!" and "You can use a function for that. All filters work the same on functions (RPC) as on views."
So you write the SQL once in the database, then call it from the client.
Option A — a VIEW (best for fixed, parameter-free queries)#
create view latest_order_per_customer as
select distinct on (customer_id) customer_id, id, total, created_at
from orders
order by customer_id, created_at desc;// query it exactly like a table
const { data, error } = await supabase
.from('latest_order_per_customer')
.select('*')Option B — an RPC function (best when you need parameters)#
create or replace function latest_orders(min_total numeric)
returns setof orders
language sql
as $$
select distinct on (customer_id) *
from orders
where total >= min_total
order by customer_id, created_at desc;
$$;const { data, error } = await supabase.rpc('latest_orders', { min_total: 100 })- You need it filtered by per-request parameters — a plain view can't take args; use an RPC function instead.
- Your table is huge with no index on
(group_col, tiebreaker)— all three approaches degrade to a full sort. Add the index first.
Supabase even has a dedicated guide that hands you the DISTINCT ON SQL: Select first row for each group. Official references: PostgreSQL SELECT / DISTINCT ON, window functions, supabase-js rpc().
Related Articles#
- How to Get a Row Count in Supabase
- Return the Inserted Row ID in supabase-js
- Supabase Postgres Functions & Triggers Guide
- How to Query Using JOIN in Supabase
- Fix Postgres 'Could Not Serialize Access' (40001)
Frequently Asked Questions#
Can supabase-js do SELECT DISTINCT ON?#
No — the query builder has no DISTINCT method. Maintainers recommend a database VIEW (query it like a table) or an RPC function (call with rpc()).
How do I get the latest row per group in Postgres?#
SELECT DISTINCT ON (group_col) * FROM t ORDER BY group_col, created_at DESC. The ORDER BY must start with the DISTINCT ON column, then the tiebreaker that picks the winner.
DISTINCT ON or ROW_NUMBER()?#
DISTINCT ON is most concise. ROW_NUMBER() in a subquery is more portable and supports top-N. LATERAL can be fastest with few groups and a good index. Measure with EXPLAIN ANALYZE.
Frequently Asked Questions
One email a month — no fluff
RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.
Continue Reading
Fix Foreign Key Constraint Violation in Supabase (23503)
23503 means a foreign key relationship is broken. Inserting a child before its parent? Insert the parent first. Can't delete a parent with children? Choose an ON DELETE action. The most common Supabase case is a profiles row referencing auth.users.
Why Your Supabase Queries Are Slow (And How to Fix)
Slow Supabase queries kill your app feel and inflate your bill. Here are the six causes I keep seeing in production apps, and the exact SQL and code fixes for each one.
Supabase vs Firebase in 2026: The Honest Comparison
Supabase vs Firebase — which backend should you pick in 2026? We compare pricing, performance, developer experience, and scalability with real benchmarks and code examples.
Browse by Topic
Find stories that matter to you.
