Skip to content
··5 min basahin

"Tama" ang Database Ko. Pero 296x Masyadong Mabagal Din.

Natuklasan kong ang Postgres database ko ay may 89 foreign keys pero zero indexes sa mga ito—ginagawang 843ms nightmares ang mga millisecond queries at halos patayin ang aking alpha launch.

Oktubre 25, 2025. Feature-complete na ang aking SaaS application. Siyam na major features, multi-tenant architecture, progressive learning — lahat ay gumagana. Pero mabagal. Talagang mabagal.

Gumagapang ang dashboard queries. Ang mga list na dapat instant na mag-load ay tumatagal ng ilang segundo. Nagsisimula nang magtanong ang mga alpha testers na "Sira ba ang site?"

Hindi lang ako nag-aalala sa performance. Nag-aalala ako sa survival.

Dalawang araw akong gumugol sa pag-optimize ng queries, pag-rewrite ng RLS policies, pagdagdag ng caching. Wala namang nagbago. Lumalaki ang frustration — 5 araw na lang bago ang planned alpha launch at hindi ko ma-figure out kung bakit napakabagal ng technically-correct na database.

Tapos nag-run ako ng isang diagnostic query na nagpabagsak ng sikmura ko.

89 foreign keys. Zero indexes.

Ito ang kwento ng isang Postgres "feature" na walang nagsasabi sa iyo — ang 2 linggo na halos pumatay sa aking alpha launch — at ang 4-minutong fix na nag-save sa lahat.

Ang Stakes: Kapag Mabagal Ay Patay

Ayon sa research, bawat 100ms ng latency ay nagkakahalaga ng 1% ng conversions. Sa 2-3 segundo na load times, hindi lang masama ang experience na ibinibigay ko — sinisira ko ang mga unang impression na magde-determine kung magtatagumpay o mabibigo ang aking alpha.

---

Ang Imbestigasyon: Pagsunod sa mga Mabagal na Queries

Nagsimula ako sa `pg_stat_statements`:

```sql
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
```

Bawat mabagal na query ay may parehong pattern — pag-filter gamit ang foreign keys:

```sql
SELECT * FROM user_data
WHERE org_id = '...' AND resource_id = '...';
```

Tiningnan ko ang execution plan:

```
Seq Scan on user_data  (cost=0.00..2847.23 rows=15 width=1024)
  Filter: ((org_id = 'abc-123') AND (resource_id = 'xyz-789'))
  Rows Removed by Filter: 12,834
Execution Time: 843.271 ms
```

Sequential scan. Binabasa ng Postgres ang bawat row sa table, tapos nagfi-filter. Sa isang query na dapat tumagal ng milliseconds.

---

Ang Rebelasyon: Hindi Awtomatikong Nag-I-Index ang Foreign Keys

Narito ang natutunan ko sa araw na iyon, sa mahirap na paraan:

Awtomatikong gumagawa ang Postgres ng indexes para sa PRIMARY KEYs at UNIQUE constraints.

HINDI awtomatikong gumagawa ang Postgres ng indexes para sa foreign keys.

Hayaan mong ulitin ko iyon, dahil ang iisang maling pag-unawa na ito ang nagkahalaga sa akin ng 2 linggo ng buhay ko:

FOREIGN KEYS ≠ INDEXES

Ang Diagnostic: Gaano Kalala?

Sumulat ako ng query para hanapin ang bawat foreign key na walang index:

```sql
SELECT
  c.conrelid::regclass AS table_name,
  a.attname AS column_name
FROM pg_constraint c
JOIN pg_attribute a ON a.attnum = ANY(c.conkey) AND a.attrelid = c.conrelid
WHERE c.contype = 'f'
  AND NOT EXISTS (
    SELECT 1 FROM pg_index i
    WHERE i.indrelid = c.conrelid AND a.attnum = ANY(i.indkey)
  );
```

Ang resulta ay parang trak na bumangga sa akin:

89 rows.

89 foreign keys sa 32 tables. Zero indexes.

---

Ang Fix: Ang Index Apocalypse

Oktubre 25, 2025. 7:51 AM. Alam ko na ang dapat kong gawin.

Isang migration. 89 indexes. Tinawag ko itong "The Index Apocalypse."

```sql
-- The Index Apocalypse Migration
CREATE INDEX idx_users_org_id ON users(org_id);
CREATE INDEX idx_resources_org_id ON resources(org_id);
CREATE INDEX idx_documents_org_id ON documents(org_id);

-- Multi-column indexes for common query patterns
CREATE INDEX idx_user_data_org_resource ON user_data(org_id, resource_id);

-- Total: 89 indexes across 32 tables
```

Pinatakbo ko ang migration. Literal na nag-hover ang kamay ko sa Enter key, nagtataka kung papalala ko ba ang lahat. T.T

Migration time: 4 minuto.

---

Ang mga Resulta: Mula Disaster Patungo sa Launch-Ready

Dati (Walang Indexes)

```sql
Seq Scan on user_data
  Execution Time: 843.271 ms
```

Pagkatapos (May Indexes)

```sql
Index Scan using idx_user_data_org_resource
  Execution Time: 2.847 ms
```

843ms → 2.8ms

Iyon ay 296x mas mabilis para sa isang query.

Real-world impact:

- Dashboard load: 2-3 segundo → 120ms

- Resource lists: 1+ segundo → 45ms

- Data queries: 850ms → 12ms

Average improvement: 20-40x mas mabilis. Ang ilang complex joins na may maraming foreign keys? 100x mas mabilis.

Gabi at araw ang pagkakaiba. Ang platform ay nagpunta mula "Sira ba ito?" patungo sa "Wow, mabilis nito." :D

---

Bakit Ito Mahalaga para sa Multi-Tenant SaaS

Kung gumagawa ka ng multi-tenant SaaS na may Row-Level Security, absolutamente kritikal ito.

Ang mga RLS policies ay tumatakbo sa bawat query:

```sql
CREATE POLICY resources_org_isolation ON resources
  USING (org_id = get_user_org_id());
```

Kung walang index sa `org_id`, nagfo-force ang policy na ito ng sequential scan sa bawat query.

Ang aral sa negosyo: Ang mga security features na walang performance optimization ay hindi talagang secure — dahil aalis ang mga users bago nila maranasan ang iyong security.

---

Mga Aral na Natutunan

1. Mapanganib ang mga assumptions sa Postgres

Inakala kong naka-index ang mga foreign keys. Hindi. Laging i-verify.

2. Kailangan ng RLS ng indexes

Walang kwenta ang Row-Level Security — talagang mas masahol pa — kung walang tamang indexes.

3. Multi-tenant = i-index ang org_id sa lahat ng dako

Sa multi-tenant architecture, lumilitaw ang `org_id` sa halos bawat query. I-index ito sa bawat table. Walang exceptions.

4. Business problem ang performance

Hindi pinapansin ng mga users na technically correct ang SQL mo kung 3 segundo ang pag-load ng page.

5. Magdagdag ng indexes nang maaga

Instant ang pagdagdag ng indexes sa mga walang laman na tables. Ang pagdagdag sa mga tables na may milyun-milyong rows ay tumatagal ng oras at nilo-lock ang table.

6. Sukatin ang lahat

Gamitin ang `EXPLAIN ANALYZE` bago at pagkatapos. Patunayan ang improvement gamit ang data.

---

Mga Huling Saloobin

89 nawawalang indexes. 2 linggo ng debugging. Isang diagnostic query para hanapin silang lahat. 4 minuto para ayusin.

Ang irony? Binibigay sa iyo ng Postgres ang lahat ng tools para ma-diagnose ito. Hindi ko lang alam na tingnan. Ngayon bawat table na ginagawa ko ay nai-index agad — foreign keys, RLS columns, sort fields, lahat.

Ang 4-minutong migration na nagdagdag ng 89 indexes ay nag-save ng ilang linggo ng hinaharap na optimization work at pumigil sa isang launch disaster.

Para sa mga founders: Hindi lang engineering problem ang performance. Conversion problem, retention problem, at credibility problem ito.

Para sa mga engineers: Hindi awtomatikong nag-i-index ng foreign keys ang Postgres. Pero dapat ikaw. Magpapasalamat ang users mo (at ang database CPU mo).

May nagkaroon ka na ba ng "technically correct pero nakapanlulumong mabagal" na sandali sa database mo? Ano ang fix — at gaano katagal bago mo nahanap?

Maraming salamat,

Chandler

Ang STRATUM architecture series: Ang performance crisis na ito ang huling piraso ng multi-tenancy puzzle na nagsimula sa pagbuo ng multi-tenancy sa Araw 2, nagpatuloy sa isang kumpletong schema rebuild sa Araw 67, at kasama ang pag-fix ng 31 blank screens mula sa nawawalang navigation context.

---

Natututo pa rin na ang "gumagana" at "gumagana nang mabilis" ay magkaibang layunin — at isa lang sa kanila ang nagshi-ship.

Ipagpatuloy ang Pagbasa

Ang Journey Ko
Kumonekta
Wika
Mga Preference