Tu PostgreSQL es el cuello de botella: cómo las PYME pueden ajustar el rendimiento sin un DBA en 2026

Your PostgreSQL Is the Bottleneck: How SMBs Can Tune Performance Without a DBA in 2026

The classic SMB story: the app “feels slow”, so you spend a weekend moving the API to a bigger cloud instance. It still crawls. Users complain, support tickets pile up, and the owner starts asking uncomfortable questions about DevOps competence. Then you check the database and find a query that’s scanning two million rows on every page load.

The database is the bottleneck in most SMB applications — and the good news is you don’t need a full-time DBA to fix it. PostgreSQL ships with powerful built-in diagnostics, and in 2026 it’s still the default open-source choice for SMB SaaS. This guide walks through a measured, low-risk tuning sequence: find the slow query, fix it with the right index, fix the connection layer, then make the gains stick. No voodoo, no reboots, no downtime.

Paso 1 — Mide antes de tocar una sola configuración

Guessing at shared_buffers before you know your slowest query is how DB tuning becomes superstition. PostgreSQL 15+ includes pg_stat_statements — enable it once and you get per-query execution stats for free:

CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

SELECT queryid,
       calls,
       round(total_exec_time / calls)::int AS avg_ms,
       round(total_exec_time / 1000)::int  AS total_s,
       left(query, 60)                     AS query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

Esa única consulta te dice exactamente qué consultas queman tu presupuesto de producción. Toma la peor y ejecuta EXPLAIN ANALYZE sobre ella:

EXPLAIN ANALYZE SELECT * FROM orders
WHERE status = 'pending' ORDER BY created_at DESC;
Seq Scan on orders  (cost=0.00..81234.11 rows=2400000 width=84)
  Filter: (status = 'pending')
Planning Time: 0.4 ms
Execution Time: 1842.3 ms

A Seq Scan on a table with thousands of rows is the smell you’re looking for: “1.8 seconds on a scan” is your smoking gun. Also switch on the slow-query log so you catch future offenders as they appear:

ALTER SYSTEM SET log_min_duration_statement = 250;

Now you have a baseline and a tripwire. Never tune blind — and never tune production data you can’t afford to lose: this is a good moment to confirm your backup strategy actually works before you start changing things.

Paso 2 — Indexa las consultas que importan

Most slow queries are fixed by one well-placed index — not by throwing CPU at PostgreSQL. The critical 2026 skill is knowing which index, because a wrong index costs you writes forever.

For our orders example, the filter is a boolean-like status plus a date sort. A plain B-tree index on status is almost useless (low cardinality). Better: a partial index that only covers the rows your query actually touches:

CREATE INDEX CONCURRENTLY idx_orders_pending_created
ON orders (created_at DESC)
WHERE status = 'pending';

Notice CONCURRENTLY — it builds the index without locking writes, so you can create it in production at 3 pm on a Tuesday (we covered zero-downtime change patterns before in zero-downtime database migrations). For dashboards that fetch a few wide columns, a covering index can make PostgreSQL answer entirely from the index:

CREATE INDEX CONCURRENTLY idx_orders_org_created
ON orders (org_id, created_at DESC)
INCLUDE (total, currency);

Re-run EXPLAIN ANALYZE and confirm the planner now picks an Index Scan:

Index Scan using idx_orders_pending_created on orders
  Execution Time: 12.4 ms   -- 1,842 ms → 12 ms

Two rules keep this safe: don’t index tables under ~10k rows (a scan is faster), and don’t stack five indexes per table. One partial index that matches the query shape beats three generic ones.

Paso 3 — Detén el agotamiento de conexiones con PgBouncer

You fixed the query, but the app still throws too many open connections around lunchtime. PostgreSQL is heavy per connection (roughly 5 MB each plus backend worker cost), so a default max_connections=100 will not survive 20 app instances with pooling disabled.

PgBouncer es la solución ligera: multiplexa unas pocas conexiones reales de base de datos sobre cientos de conexiones de clientes usando pooling a nivel de transacción:

# pgbouncer.ini
[databases]
acme = host=/var/run/postgresql

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
pool_mode = transaction
default_pool_size = 30
max_client_conn = 1000
pgbouncer /etc/pgbouncer/pgbouncer.ini
# app connection string: postgres://user:pass@db:6432/acme

Put PgBouncer in front of PostgreSQL, point your app pool at port 6432, and watch the “connection refused” alerts disappear. A good starting pool size is max_connections × 0.5 for the app, and leave headroom for maintenance connections. When you do catch a runaway query during a spike, knowing how to stop it calmly is half the battle:

SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'active'
  AND query ILIKE '%orders%'
  AND now() - query_start > interval '30 seconds';

Paso 4 — Victorias baratas: memoria, autovacuum y bloat

Los índices arreglaron el síntoma; estas configuraciones previenen el siguiente. En un servidor de base de datos dedicado, empieza con memoria razonable:

ALTER SYSTEM SET shared_buffers = '2GB';       -- ~25% of RAM
ALTER SYSTEM SET effective_cache_size = '6GB'; -- ~75% of RAM
ALTER SYSTEM SET work_mem = '16MB';            -- don't max this out

work_mem is per-operation and per-session — a 1 GB setting across 50 connections is a memory bomb, not a speedup. Keep it modest.

Then handle bloat. PostgreSQL’s MVCC means updated rows leave dead tuples that only autovacuum collects. The 2026 defaults are reasonable, but on write-heavy tables you’ll see table size ballooning:

SELECT pg_size_pretty(pg_total_relation_size('orders'));

ALTER SYSTEM SET autovacuum_vacuum_scale_factor = 0.05; -- tighter than 0.2
ALTER SYSTEM SET autovacuum_naptime = '30s';

If a table is already bloated, a maintenance window with VACUUM (ANALYZE, VERBOSE) orders; (or pg_repack when it’s chronic) reclaims space and refreshes planner statistics. Schedule it during low traffic and watch your query plans improve.

Keep the Gains — and Know When It’s Not the Database

Performance tuning is a practice, not a one-off. Add a 15-minute weekly review: top 5 queries from pg_stat_statements, slow-log scan, bloat check. Feed those numbers into your observability layer so a regressing query alerts you before users do — the OpenTelemetry-based observability stack we recommend for SMBs makes this straightforward.

Also know your boundaries: if the app does N+1 queries in the code, no index will save you. If one hot table outgrows a single node, that’s a scale-out decision (read replicas, partitioning), not a tuning problem. The process above gets you 80% of the way for free — the last 20% is architecture, and that’s a conversation worth having with someone who’s seen your exact stack before.

Want us to audit your PostgreSQL before the next launch? Book a free 30-minute DevOps consultation at /reserva-cita.

es_ESEspañol
Scroll al inicio