Fetching Only What You Need: SELECT * vs Explicit Columns in Supabase
Learn how SELECT * works in Supabase, why extra columns grow JSON and network cost, and when to select only the fields your UI needs.

When you fetch data from Supabase, it is easy to write .select('*') and move on. It works — you get every column back. The problem is not that SELECT * is always slow in PostgreSQL. It is that it often returns more data than your UI uses. That extra payload still leaves the database, becomes JSON, travels over the network, and gets parsed on the client. At scale, that adds up.
This post covers what SELECT and SELECT * mean, how a query travels through Supabase, and where extra columns actually cost you time — so you can fetch only what you need.
Quick Answer
SELECT * tells PostgreSQL to return every column for the rows that match your filter. If the screen only needs two or three fields, the rest still become JSON and cross the network. Prefer listing the columns you need. More columns do not always make the database plan much slower — they usually make the response larger, which hits serialization, network transfer, and unused client work. One quiet query a day barely matters; thousands of over-fetches do.

Prerequisites
- A Supabase project with a PostgreSQL table you can query
- Basic familiarity with SQL
SELECT/WHERE/LIMIT - The Supabase JavaScript client (or any client that calls the Data API)
- Optional: access to the SQL editor for
EXPLAIN ANALYZE, and browser DevTools → Network
For a fuller walkthrough of projects, PostgreSQL, and core features, see Supabase Fundamentals: Database, PostgreSQL, Projects, and Core Features (Rasik’s blog — add URL when published).
What is SELECT in PostgreSQL and Supabase?
Supabase stores application data in PostgreSQL. Data lives in tables (columns and rows). Schema changes usually go through migration files. Inserts store data; `SELECT` reads it.
With SELECT you choose:
- Which table to read
- Which columns to return
- Which rows to include (filters, limits, ordering)
// All columns
supabase.from('users').select('*')
// Only what the UI needs
supabase.from('users').select('first_name, last_name')Both are valid. The difference is how much data comes back per matching row.
The problem: SELECT * ships more than the UI needs
SELECT * means: return all columns for each matching row.
A list screen may only need first_name and last_name, while the users table also holds email, phone, date of birth, flags, timestamps, referrals, address lines, and more. With SELECT *, every field is included for every returned row — even if the UI never renders them.
That extra width still has to be read, converted to JSON, transferred, and parsed. On a local or nearby database it may feel fine. Across regions or slower networks — and under high request volume — larger responses cost more.
We are not claiming SELECT * always produces a slow query plan. The main issue is that it can force the application path to carry more data than it needs.
Rows vs columns (keep these separate)
| What you count | What it means | Example |
|---|---|---|
| Rows | How many records match the query | 100 users in a list |
| Columns | How many attributes come back per row | 2 name fields vs ~30 on users |
Filters and LIMIT decide how many rows. The column list decides how wide each row is in the response.
Suppose:
users table
↓
1,000,000 total rows
↓
100 rows match WHERE is_active = true (LIMIT 100)The table has 30 columns.
Query 1 — all columns:
SELECT *
FROM users
WHERE is_active = true
LIMIT 100;100 matching rows × 30 columns → large resultQuery 2 — only what the UI needs:
SELECT first_name, last_name
FROM users
WHERE is_active = true
LIMIT 100;100 matching rows × 2 columns → smaller resultSame filter. Same LIMIT. Same row count. Different width. The million total rows matter for finding matches (indexes, planning). After those hundred are chosen, SELECT * vs two columns is about how much of each row you ship.
Illustrative response size
Exact bytes depend on your data. For teaching, for 100 rows:
| Query | Columns per row | Ballpark JSON body (illustrative) |
|---|---|---|
| SELECT * (≈30 columns) | 30 | ~80–150 KB |
| SELECT first_name, last_name | 2 | ~5–15 KB |
These are examples, not your production numbers. Measure in the Network tab (below).

What happens when your app queries Supabase
A typical read from React (or another client) does not open a raw Postgres connection in the browser. It goes through the Supabase client and the Data API (PostgREST).

In short:
- Application — needs data (posts, users, and so on).
- Supabase Client — e.g.
supabase.from('posts').select('*'). - Supabase Data API / PostgREST — HTTPS
/rest/v1/.... - PostgreSQL — runs the SQL.
- PostgREST formats — rows → JSON.
- HTTP response — e.g.
200 OK,application/json. - Network — body travels to the client.
- Application — parses JSON and renders.
Extra columns grow work from steps 4–8: more data to format, a larger body, more transfer, more unused fields to parse.
Does selecting more columns affect database execution?
Compare:
SELECT * FROM users WHERE is_active = true LIMIT 100;
-- vs --
SELECT first_name, last_name FROM users WHERE is_active = true LIMIT 100;One focused question: can selecting more columns change how Postgres executes the query?
What Postgres does (short)
- Find matching rows —
WHERE, indexes, query planner. - Project columns —
*vs an explicit list. - Return the result — to PostgREST (in the usual Supabase path).
Finding rows is driven mostly by filters, indexes, and the planner. Shipping columns is where width shows up (more heap/TOAST I/O, more data to the API).
Is more columns always slower in Postgres?
Not always in a noticeable way. For many everyday queries with a modest LIMIT, plans can look similar. Extra columns can still cost more on wide tables or large text/JSON fields.
Other factors often dominate “* vs two columns”:
- Indexes for
WHERE/ORDER BY - Filter selectivity
- How many rows you return
- Cache, sorts, joins, RLS
Check with EXPLAIN ANALYZE
EXPLAIN ANALYZE
SELECT *
FROM users
WHERE is_active = true
LIMIT 100;
EXPLAIN ANALYZE
SELECT first_name, last_name
FROM users
WHERE is_active = true
LIMIT 100;Simplified shape (yours will differ):
Limit (actual time=0.8..1.4 rows=100 loops=1)
-> Index Scan using users_is_active_idx on users
Planning Time: 0.2 ms
Execution Time: 1.5 msCompare plan shape and execution time. If they are close, column width is probably not your main Postgres bottleneck — payload after Postgres still matters.
When fewer columns can change the plan
If an index covers the columns you need, Postgres may use an index-only scan. SELECT * often needs columns outside that index, so Postgres may visit the heap. You do not need covering indexes everywhere — just know explicit columns can help on some schemas. Confirm with EXPLAIN ANALYZE.
Bottom line: more columns can cost more inside Postgres; they do not always mean a dramatically different plan. Measure DB time; assume payload size grows with SELECT *.
How PostgreSQL becomes an API response in Supabase
PostgREST exposes PostgreSQL as a REST HTTP API. A client request becomes SQL; a SQL result becomes an HTTP JSON body.

Application
→ HTTP request (Supabase Data API / PostgREST)
→ PostgreSQL
→ result (rows × selected columns)
← PostgREST formats JSON
← HTTP responseYou do not receive a raw database object in the browser. Unused columns still matter: they were selected, serialized, and included in the body.
Docs: PostgREST, Supabase Database. Related reading when published: Supabase Edge Functions vs Backend (Romil), Supabase RLS (Ritul).
Does selecting more columns increase response size on the network?
Yes. More columns → larger JSON → more bytes on the wire. Better networks reduce pain; they do not remove the bytes.
How to check in the browser
- DevTools → Network
- Trigger the Supabase query
- Open
/rest/v1/... - Note Size for
.select('*') - Switch to
.select('first_name, last_name'), reload, compare
If size drops and the UI still works, you were shipping unused columns.
Security: SELECT * can overshare
Wide tables often hold fields you never meant to send to the browser (emails, phones, IDs, flags, address lines). SELECT * puts selected columns into the JSON — visible in DevTools, logs, and client memory.
RLS (Ritul’s blog — add URL when published) limits which rows a user sees. It does not strip columns you selected. Explicit column lists are least privilege for API responses as well as a performance habit.
What happens on the client when unused fields arrive?

HTTP response → network → app receives JSON
→ JSON.parse → JavaScript objects
→ UI uses only the fields it needsIf the payload has 20 fields per row and the screen needs 2, the client still pays for parsing, memory, object creation, and heavier list rendering. One quiet request may hide it; busy lists and mobile clients will not.
When this matters (and when it does not)
| Situation | Impact of SELECT * |
|---|---|
| Rare admin query, narrow table | Usually fine |
| Hot list/detail screens, wide tables | Worth fixing |
| Mobile / variable networks | Payload size matters more |
| High QPS returning large JSON | Serialization + bandwidth add up |
Practical habit (implementation)
Prefer explicit columns for UI-driven queries:
const { data, error } = await supabase
.from('users')
.select('id, first_name, last_name')
.eq('is_active', true)
.order('created_at', { ascending: false })
.limit(100)Use SELECT * when you truly need the full row (edit forms, admin tools, SQL exploration). Also control rows: filters, pagination (range / limit), and indexes.
Common problems
Everything feels fine locally, but production feels heavy Local or same-region DBs hide transfer cost. Compare Network sizes in environments closer to real users.
“We have a million rows, so SELECT * must be reading them all” Not if WHERE + LIMIT return 100 rows. Table size affects finding matches; selected columns affect response width.
Blaming only Postgres Plans may look similar while JSON and network still grow. Check both EXPLAIN ANALYZE and response size.
RLS is enabled, so over-fetch is safe RLS is about rows, not column projection. You can still overshare fields on allowed rows.
Defaulting every query to `.select('*')` Convenient in prototypes; costly as a production default for list and card UIs.
Best practices
- List columns the screen needs for list/card queries.
- Use
SELECT *only when the full row is required. - Paginate and filter — fix width and row count.
- Compare
/rest/v1response sizes when changing selects. - Use
EXPLAIN ANALYZEwhen you suspect database-time cost. - Treat unused client fields as a privacy risk, not only a performance issue.
- Avoid making
.select('*')the team default in shared query helpers.
FAQ
Is SELECT * always slow in Postgres? No. Many plans look similar. The consistent cost is larger JSON and more network/client work.
If my table has 1,000,000 rows, does SELECT * read all of them? Not when WHERE and LIMIT only return 100. Total size affects finding matches; columns affect width.
Should I never use SELECT *? Use it for full-row needs. Avoid it as the default for read-only lists.
Does a faster network fix over-fetching? It reduces pain, not bytes, parse cost, memory, or accidental exposure.
Can RLS replace selecting fewer columns? No. Use both.
How do I prove this on my app? Compare Network sizes for .select('*') vs explicit columns; optionally compare EXPLAIN ANALYZE.
Conclusion
SELECT reads rows from PostgreSQL. SELECT * returns every column for those rows — fine for a full record, wasteful when the UI needs a few fields. In Supabase, unused columns still move through PostgREST, HTTP, the network, and the client. Separate row count from column count, fetch what the screen needs, and keep SELECT * for cases that truly require the full row.
Need help reviewing Supabase queries, payload size, or Postgres schema design for your application? Our team works with production Supabase and PostgreSQL setups. Contact us to discuss your project.

Aarav Sharma
Lead Software Engineer
Aarav leads product engineering at Matlab Infotech, where he has shipped mobile and web platforms across healthcare, fintech, and SaaS. He writes about pragmatic engineering and shipping fast without cutting corners.


