0Pricing
PostgreSQL Performance & Query Optimization · Ders

Havuzları Çekirdek Sayısına Göre Boyutlandırma

Aşırı bağlam değiştirmeyi önlemek için havuz ve max_connections sınırlarını CPU ile iş yükünden türetin.

Havuzları Çekirdek Sayısına Göre Boyutlandırma, CoddyKit'te ücretsiz bir PostgreSQL Performance & Query Optimization dersidir. Bu, 4 dersinin 3. dersidir. Aşağıdan dersin tamamını ücretsiz okuyabilir, sonra tarayıcıda yerleşik kod editörü ve 7/24 yapay zeka koçu ile uygulamalı olarak pratik yapabilirsin. Bu, PostgreSQL Performance & Query Optimization öğrenme yolunun bir parçasıdır ve ilerlemeniz web ve CoddyKit uygulaması arasında senkronize olur. PostgreSQL Performance & Query Optimization kursu toplamda 4 dersten oluşur.

Bu dersin bazı bölümleri henüz çevrilmemiş olup İngilizce olarak gösterilmektedir.

Why Pool Size Is Not Connection Count

A common mistake is treating the connection pool as a buffer you can grow freely. With PgBouncer in front of PostgreSQL, you actually run two limits: how many clients can talk to PgBouncer, and how many server connections PgBouncer keeps open to PostgreSQL.

  • max_client_conn can be large (thousands) — these are cheap proxied sockets.
  • default_pool_size (and PostgreSQL max_connections) is the expensive number — each one is a real backend process.

This lesson is about choosing that expensive number from your CPU core count and workload, so the database does real work instead of thrashing between too many backends.

One Backend = One Process

Each PostgreSQL connection is backed by a dedicated OS process. When you have more active backends than CPU cores, the kernel time-slices them. Past a point, adding connections does not add throughput — it adds context switches, lock contention, and memory pressure.

You can see how many backends exist right now and how many are actually running queries:

SELECT state, count(*)
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY state
ORDER BY count(*) DESC;

The Starting Formula

The widely cited baseline for a CPU-bound, mostly-active workload is:

  • connections = (core_count * 2) + effective_spindle_count

The * 2 accounts for backends that briefly stall on I/O or locks while others use the CPU. The effective_spindle_count approximates how many concurrent I/O operations your storage can absorb (think 0 for a fully cached working set, higher for many-disk arrays).

For an 8-core server on SSD with a mostly-cached dataset, this lands around 16-20 server connections — not 200.

Computing It In SQL

You don't have to do the arithmetic by hand. PostgreSQL exposes detected core counts, so you can compute a starting pool size directly. The query below is self-contained and runs anywhere:

WITH params AS (
  SELECT 8::int  AS core_count,
         0::int  AS effective_spindles
)
SELECT core_count,
       effective_spindles,
       (core_count * 2) + effective_spindles AS suggested_connections
FROM params;

Active vs Idle Backends

The formula sizes for active work. Pools fail in practice because of backends parked in idle in transaction — they hold a slot (and often locks) without doing anything. These eat your budget just as much as busy queries.

Audit them before you size up the pool:

SELECT pid,
       state,
       now() - state_change AS idle_for,
       wait_event_type,
       left(query, 60) AS query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY idle_for DESC;

Pool Mode Changes Everything

How aggressively PgBouncer reuses server connections depends on pool_mode:

  • session: a server connection is tied to a client for its whole session. You need roughly as many server connections as concurrent clients — pooling buys little.
  • transaction: a server connection is returned after each transaction. A small pool can serve many clients. This is what lets (cores*2)-sized pools handle thousands of clients.
  • statement: returned after each statement; most aggressive, but forbids multi-statement transactions.

Sizing against core count assumes transaction mode for OLTP workloads.

A Realistic PgBouncer Block

Putting the numbers together for an 8-core OLTP database, a typical pgbouncer.ini looks like this. Note how max_client_conn is huge while default_pool_size stays near the formula's output:

-- pgbouncer.ini (excerpt)
-- pool_mode = transaction
-- max_client_conn = 2000
-- default_pool_size = 20
-- reserve_pool_size = 5
-- reserve_pool_timeout = 3
-- For an 8-core box: (8 * 2) + 0 = 16, rounded to 20.

max_connections Must Cover Every Pool

PostgreSQL's max_connections is a hard ceiling across all PgBouncer pools plus reserved superuser slots. If you run several databases/users, each gets its own pool of up to default_pool_size, and they all draw from the same backend budget.

Rule of thumb: max_connections ≥ sum of all pool sizes + reserve_pool_size + superuser_reserved_connections + a margin for maintenance and replication.

SHOW max_connections;

SELECT current_setting('max_connections')::int            AS max_conn,
       current_setting('superuser_reserved_connections')::int AS reserved,
       current_setting('max_connections')::int
         - current_setting('superuser_reserved_connections')::int AS usable;

Memory Is The Other Budget

Cores cap useful concurrency, but RAM caps how high max_connections can safely go. Each backend can allocate up to work_mem per sort/hash node, and a single query may use it several times over.

  • Worst case ≈ max_connections * work_mem * (nodes per query).
  • Set work_mem with the real connection ceiling in mind — a small pool lets you afford a larger work_mem.

This is a strong argument for pooling: fewer backends means more memory per query.

SELECT current_setting('work_mem')                       AS work_mem,
       current_setting('max_connections')::int           AS max_conn,
       pg_size_pretty(
         current_setting('work_mem')::bigint
         * current_setting('max_connections')::int
       ) AS naive_worst_case;

Validate Against Real Saturation

The formula is a starting point, not gospel. After deploying, watch whether backends are CPU-bound (good — cores are the limit) or stuck on LWLock/Lock waits (a sign the pool is too large and contention is rising).

Sample the wait events under load:

SELECT coalesce(wait_event_type, 'Running') AS wait_type,
       coalesce(wait_event, 'on_cpu')       AS wait_event,
       count(*)
FROM pg_stat_activity
WHERE state = 'active'
  AND backend_type = 'client backend'
GROUP BY 1, 2
ORDER BY count(*) DESC;

Tuning Loop In Practice

Use a tight feedback loop instead of guessing:

  • Start at (cores * 2) for default_pool_size.
  • Load test. If throughput is flat and latency rises while CPUs are saturated, the pool is already big enough — shrink it.
  • If CPUs sit idle while clients queue at PgBouncer (rising cl_waiting), the pool may be too small or queries are I/O-bound — raise effective_spindle_count and retest.

Check PgBouncer's own view of pressure with the admin console:

-- Connect to the special 'pgbouncer' admin database, then:
SHOW POOLS;
-- Watch cl_active, cl_waiting, sv_active, sv_idle.
-- Persistent cl_waiting > 0 with idle CPUs => pool too small.

Quick Check

You have a 16-core PostgreSQL server, NVMe storage, and a working set that fits entirely in RAM (effectively zero spindles). The app currently opens 800 direct connections and CPUs are pegged with rising lock waits. Using the standard sizing approach with PgBouncer in transaction mode, what is the best starting default_pool_size?

Recap

Key takeaways for sizing pools against core count:

  • Separate the cheap limit (max_client_conn) from the expensive one (default_pool_size / max_connections).
  • Start from (cores * 2) + effective_spindle_count — usually tens of connections, not hundreds.
  • The formula assumes transaction pool mode; session mode needs far more server connections.
  • Ensure max_connections covers the sum of all pools plus reserved slots, and budget RAM via work_mem * max_connections.
  • Hunt down idle in transaction backends and validate with real wait-event and SHOW POOLS data — shrink when CPU-bound, only grow when CPUs idle and clients queue.

Sıkça Sorulan Sorular

“Havuzları Çekirdek Sayısına Göre Boyutlandırma” dersi ücretsiz mi?

Evet — “Havuzları Çekirdek Sayısına Göre Boyutlandırma” dersin tüm metni burada web'de ücretsiz olarak okunabilir. Etkileşimli olarak pratik yapmak (yerleşik kod editörü ve 7/24 yapay zeka koçu) ve PostgreSQL Performance & Query Optimization kursunun geri kalanını açmak için CoddyKit PRO'ya yükselt. PostgreSQL Performance & Query Optimization kursu toplamda 4 dersten oluşur.

“Havuzları Çekirdek Sayısına Göre Boyutlandırma” dersinde ne öğreneceğim?

Aşırı bağlam değiştirmeyi önlemek için havuz ve max_connections sınırlarını CPU ile iş yükünden türetin. PostgreSQL Performance & Query Optimization ile uygulamalı kodu tarayıcıda doğrudan çalıştırarak pratik yaparsın ve 7/24 yapay zeka koçu dersi çalışırken sorularını yanıtlar.

PostgreSQL Performance & Query Optimization öğrenmeye başlamak için deneyim gerekli mi?

Önceden deneyim gerekmez. CoddyKit'te PostgreSQL Performance & Query Optimization, başlangıçtan ileri seviyeye kadar yapılandırıldığı için buradan başlayabilir veya başından başlayıp kendi hızında ilerleme yapabilirsin. Bu, 4 dersinin 3. dersidir.

“Havuzları Çekirdek Sayısına Göre Boyutlandırma” dersi ne kadar sürer?

Çoğu CoddyKit dersi yaklaşık 5–10 dakika sürer. Her biri kısa ve etkileşimli olduğu için sabit ilerleme yaparsın ve web ile uygulama arasında tam olarak bıraktığın yerden devam edebilirsin.

Bu PostgreSQL Performance & Query Optimization dersinde kod yazıp çalıştırabilir miyim?

Evet. Her PostgreSQL Performance & Query Optimization dersi yerleşik bir kod editörü içerir, bu sayede tarayıcıda gerçek kod yazıp çalıştırabilir ve anlık yapay zeka geri bildirimi alırsın — yerel kurulum gerekli değildir.

Bu kursun tüm dersleri

  1. PostgreSQL'de Bağlantılar Neden Maliyetlidir
  2. İşlem ve Oturum Havuzlama Kipleri
  3. Havuzları Çekirdek Sayısına Göre Boyutlandırma
  4. Havuz Doygunluğunu ve Kuyrukları Tanılama
← PostgreSQL Performance & Query Optimization Sayfasına Dön