<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
  <channel>
    <title>Microsoft Blog for PostgreSQL articles</title>
    <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/bg-p/ADforPostgreSQL</link>
    <description>Microsoft Blog for PostgreSQL articles</description>
    <pubDate>Sat, 02 May 2026 07:33:07 GMT</pubDate>
    <dc:creator>ADforPostgreSQL</dc:creator>
    <dc:date>2026-05-02T07:33:07Z</dc:date>
    <item>
      <title>Potential Consequences of Using Postgres as a Job Queue</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/potential-consequences-of-using-postgres-as-a-job-queue/ba-p/4514332</link>
      <description>&lt;H2 aria-level="1"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;Introduction&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;At small&amp;nbsp;scale, using Postgres as a job queue is&amp;nbsp;totally fine, and&amp;nbsp;I’d&amp;nbsp;even say&amp;nbsp;it’s&amp;nbsp;the right call. Fewer moving parts, one less system to manage, ACID guarantees on your jobs.&amp;nbsp;What’s&amp;nbsp;not to love?&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;The problem is that “small scale” has a ceiling, and the ceiling is lower than most people expect. When you’ve got thousands of concurrent workers hammering a jobs table with&lt;/SPAN&gt; &lt;SPAN data-contrast="auto"&gt;&lt;CODE&gt;SELECT ... FOR UPDATE SKIP LOCKED&lt;/CODE&gt;&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;, things start to behave in ways that aren’t obvious from the application layer. CPU usage creeps up. Also vacuum sometimes can’t keep up. Finally, in the wait event stats, you start seeing ominous entries like &lt;CODE&gt;LWLock:MultiXactSLRU&lt;/CODE&gt; stacking up across many backends.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;This pattern&amp;nbsp;has&amp;nbsp;tripped&amp;nbsp;up teams more than a few times, and it usually plays out the same way: everything works fine in dev and staging, then goes off a cliff in production once the concurrency gets real.&amp;nbsp;So&amp;nbsp;let’s&amp;nbsp;dig into why this happens, and what the alternatives look like.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2 aria-level="1"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;The Typical Pattern&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;When using Postgres as a job queue, the standard approach looks something like this:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;CREATE TABLE job_queue ( 
    id         bigserial PRIMARY KEY, 
    status     text NOT NULL DEFAULT 'pending', 
    payload    jsonb NOT NULL, 
    created_at timestamptz NOT NULL DEFAULT now(), 
    locked_by  text, 
    locked_at  timestamptz 
); 
 
CREATE INDEX idx_job_queue_status ON job_queue (status) WHERE status = 'pending'; &lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Workers grab jobs with:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;UPDATE job_queue 
   SET status = 'processing', 
       locked_by = 'worker-42', 
       locked_at = now() 
WHERE id = ( 
     SELECT id FROM job_queue 
      WHERE status = 'pending' 
      ORDER BY created_at 
      LIMIT 1 
        FOR UPDATE SKIP LOCKED 
) 
RETURNING *; &lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;And then mark them done:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;UPDATE job_queue SET status = 'completed' WHERE id = $1; &lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Some users may &lt;/SPAN&gt;&lt;CODE&gt;&lt;SPAN data-contrast="auto"&gt;DELETE&lt;/SPAN&gt;&lt;/CODE&gt; &lt;SPAN data-contrast="auto"&gt;the row entirely. Either way, the lifecycle is: insert, lock-and-update, update-or-delete. Repeated&amp;nbsp;thousands of times per second.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;At low concurrency, this works&amp;nbsp;very smoothly.&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; &lt;CODE&gt;SKIP LOCKED&lt;/CODE&gt; &lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;means workers&amp;nbsp;don’t&amp;nbsp;block each other waiting for the same row. Postgres handles the locking, visibility, and ordering.&amp;nbsp;It’s&amp;nbsp;elegant.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;So where does it break?&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2 aria-level="1"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;The&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;MultiXact&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;&amp;nbsp;SLRU Problem&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;When multiple transactions hold locks on the same row, Postgres stores the set of lockers as a&amp;nbsp;MultiXact&amp;nbsp;ID – a pointer into a side structure under&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt; &lt;CODE&gt;pg_multixact/&lt;/CODE&gt;&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;With &lt;/SPAN&gt;&lt;CODE&gt;&lt;SPAN data-contrast="auto"&gt;SELECT ... FOR UPDATE SKIP LOCKED&lt;/SPAN&gt;&lt;/CODE&gt;&lt;SPAN data-contrast="auto"&gt;, users might think MultiXacts aren’t involved – after all, &lt;/SPAN&gt;&lt;CODE&gt;&lt;SPAN data-contrast="auto"&gt;SKIP LOCKED&lt;/SPAN&gt;&lt;/CODE&gt;&lt;SPAN data-contrast="auto"&gt; is supposed to avoid contention. But in practice, with many concurrent workers all racing to lock rows, there are brief windows where multiple transactions reference the same row before one of them “wins” and the others skip. If you combine this with any &lt;/SPAN&gt;&lt;CODE&gt;&lt;SPAN data-contrast="auto"&gt;FOR SHARE&lt;/SPAN&gt;&lt;/CODE&gt;&lt;SPAN data-contrast="auto"&gt; or &lt;/SPAN&gt;&lt;CODE&gt;&lt;SPAN data-contrast="auto"&gt;FOR KEY SHARE&lt;/SPAN&gt;&lt;/CODE&gt;&lt;SPAN data-contrast="auto"&gt;&amp;nbsp;locks (which are commonly created implicitly by foreign key checks),&amp;nbsp;MultiXact&amp;nbsp;IDs start accumulating&amp;nbsp;quickly.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;The&amp;nbsp;MultiXact&amp;nbsp;data lives in SLRU buffers (Simple Least Recently Used) – a small, fixed-size shared memory cache. When backends need to read or write&amp;nbsp;MultiXact&amp;nbsp;data, they&amp;nbsp;acquire&amp;nbsp;LWLocks&amp;nbsp;to access these buffers. Under high concurrency, this becomes a bottleneck:&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;PRE&gt;&lt;SPAN data-contrast="auto"&gt; wait_event_type | wait_event&lt;/SPAN&gt;&amp;nbsp;&lt;BR /&gt;&lt;SPAN data-contrast="auto"&gt;-----------------+-------------------&lt;/SPAN&gt;&amp;nbsp;&lt;BR /&gt;&lt;SPAN data-contrast="auto"&gt; LWLock&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; | MultiXactMemberSLRU&lt;/SPAN&gt;&amp;nbsp;&lt;BR /&gt;&lt;SPAN data-contrast="auto"&gt; LWLock&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; | MultiXactOffsetSLRU&lt;/SPAN&gt; &lt;/PRE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;You’ll see dozens or hundreds of backends piled up on these waits. The SLRU cache is small (by design – it’s a fixed number of pages in shared memory), and when the working set of MultiXact lookups exceeds what fits in the cache, you get constant eviction and re-reads from disk. Every lock acquisition and release on a job row potentially triggers a MultiXact SLRU lookup, and at thousands of concurrent sessions, those lookups serialize on LWLocks.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;The result: CPU gets pegged, throughput collapses, and latency spikes – not because the queries are expensive, but because the locking infrastructure itself is overwhelmed.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-charstyle="Heading 1 Char"&gt;Bloat: The Silent Killer&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;The other side of this coin is table and index bloat. Every job row goes through multiple updates (and possibly a delete), and each of those operations creates a new tuple version in the heap. The old versions stick around until &lt;/SPAN&gt;&lt;CODE&gt;&lt;SPAN data-contrast="auto"&gt;VACUUM&lt;/SPAN&gt;&lt;/CODE&gt; &lt;SPAN data-contrast="auto"&gt;cleans them up.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;On a busy job queue table:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Dead tuples accumulate faster than&amp;nbsp;autovacuum&amp;nbsp;can clean them. By the time&amp;nbsp;autovacuum&amp;nbsp;finishes one pass, tens of thousands of new dead tuples have appeared. The table grows and grows.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Index bloat compounds the problem. Every index on the table also accumulates dead entries. The partial index on status = 'pending' gets thrashed especially hard, since rows constantly enter and leave that condition.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Sequential scans get slower. As the table bloats, even index scans start doing more I/O because the heap pages are sparsely populated. Vacuum reclaims space at the end of the&amp;nbsp;table, but&amp;nbsp;can’t&amp;nbsp;reclaim space in the middle (unless the pages are completely empty).&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Job queue tables&amp;nbsp;can&amp;nbsp;grow to tens of gigabytes when the actual “live” data&amp;nbsp;was&amp;nbsp;only a few megabytes.&amp;nbsp;It makes everything slower: scans, vacuum, even&amp;nbsp;pg_dump.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;You can mitigate this by running vacuum more aggressively (lower &lt;CODE&gt;autovacuum_vacuum_scale_factor&lt;/CODE&gt;, higher &lt;CODE&gt;autovacuum_vacuum_cost_limit&lt;/CODE&gt;), or by partitioning the table and dropping old partitions. But at some point, you’re fighting the fundamental mismatch between MVCC’s design goals and the write pattern of a job queue.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-charstyle="Heading 1 Char"&gt;CPU and Lock Overhead&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Beyond the SLRU contention and bloat,&amp;nbsp;there’s&amp;nbsp;just the raw overhead of using Postgres’s full transactional machinery for what is&amp;nbsp;essentially a&amp;nbsp;FIFO dispatch operation:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Every lock/unlock is a full WAL-logged transaction. Grabbing a job writes WAL. Marking it complete writes WAL. Deleting it writes WAL. On a system processing thousands of jobs per second, the WAL volume from the job queue alone can saturate your&amp;nbsp;wal_writer&amp;nbsp;and checkpoint processes.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;&lt;CODE&gt;SKIP LOCKED&lt;/CODE&gt;&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;&amp;nbsp;still touches rows. The name suggests rows are skipped, but Postgres still&amp;nbsp;has to&amp;nbsp;find them, check their lock status, and move on. With high concurrency, many workers end up scanning past the same locked rows before finding one they can claim. This is&amp;nbsp;wasted&amp;nbsp;CPU.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Snapshot management overhead also becomes an issue. Each transaction needs a consistent snapshot, and with thousands of concurrent transactions, the ProcArray (the structure that tracks active transactions) becomes a contention point itself. You might see &lt;CODE&gt;LWLock:ProcArrayLock&lt;/CODE&gt; waits alongside the MultiXact ones.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Vacuum contention. While&amp;nbsp;vacuum is&amp;nbsp;cleaning up dead tuples, it needs locks too. On a table under constant write pressure, vacuum can interfere with the workers and vice versa.&amp;nbsp;I’ve&amp;nbsp;seen systems where disabling&amp;nbsp;autovacuum&amp;nbsp;on the job queue&amp;nbsp;table&amp;nbsp;improved&amp;nbsp;throughput&amp;nbsp;in the short term.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2 aria-level="1"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;Better Alternatives&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;So&amp;nbsp;what should you use instead? It depends on your requirements, but there are several options that handle high-throughput job dispatch more gracefully than a Postgres table.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-charstyle="Heading 2 Char"&gt;Advisory Locks (Staying in Postgres)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;If you want to stay within Postgres and avoid adding infrastructure, advisory locks are worth&amp;nbsp;considering for&amp;nbsp;certain queue patterns. Instead of locking rows, you lock on an abstract numeric key:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;-- Worker tries to acquire a lock on the job ID 
SELECT pg_try_advisory_lock(id) FROM job_queue 
WHERE status = 'pending' 
ORDER BY created_at 
LIMIT 1;&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Advisory locks are lightweight – they don’t touch the heap, don’t create MultiXact entries, and don’t generate dead tuples. They live entirely in shared memory. The trade-off is that you lose the atomicity of &lt;/SPAN&gt;&lt;CODE&gt;&lt;SPAN data-contrast="auto"&gt;FOR UPDATE SKIP LOCKED&lt;/SPAN&gt;&lt;/CODE&gt;&lt;SPAN data-contrast="auto"&gt;: you need to handle the case where a lock is&amp;nbsp;acquired&amp;nbsp;but the job processing fails, and you need to release the lock explicitly (or rely on session-end cleanup).&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;This approach works well when the queue depth is&amp;nbsp;manageable&amp;nbsp;and you want to avoid the MVCC overhead. But&amp;nbsp;it’s&amp;nbsp;still Postgres, so&amp;nbsp;you’re&amp;nbsp;still subject to connection limits,&amp;nbsp;ProcArray&amp;nbsp;overhead, and general resource contention at&amp;nbsp;very high&amp;nbsp;session counts.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3 aria-level="2"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;pgq&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;&amp;nbsp;(&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;Skytools&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;)&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;pgq&amp;nbsp;is purpose-built for exactly this problem.&amp;nbsp;It’s&amp;nbsp;a queue implementation that sits inside Postgres but uses a batching model that avoids most of the row-level locking and MVCC pitfalls. Events are written to a queue table, but consumers read them in&amp;nbsp;batches&amp;nbsp;and the queue maintenance is done via a ticker process that manages rotation.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;The key advantages:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="2" data-list-defn-props="{" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;No row-level contention. Consumers&amp;nbsp;don’t&amp;nbsp;lock individual rows.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="2" data-list-defn-props="{" data-aria-posinset="2" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Built-in batch processing. Events are consumed in chunks, reducing transaction overhead.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="2" data-list-defn-props="{" data-aria-posinset="3" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Efficient cleanup. Old events are rotated out rather than vacuumed row-by-row.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;The downside is that&amp;nbsp;pgq&amp;nbsp;is not as actively&amp;nbsp;maintained&amp;nbsp;as it once was, and it adds operational complexity (the ticker daemon, consumer registration, etc.). But for teams already deep in the Postgres ecosystem,&amp;nbsp;it’s&amp;nbsp;a battle-tested&amp;nbsp;option.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3 aria-level="2"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;Redis&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;For many teams, Redis is the natural choice for job queues. Using Redis lists (BRPOPLPUSH or the Streams API), you get:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="8" data-list-defn-props="{" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Sub-millisecond dispatch latency. No disk I/O, no MVCC, no vacuum.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="8" data-list-defn-props="{" data-aria-posinset="2" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Atomic pop operations. Workers grab jobs without any locking protocol.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="8" data-list-defn-props="{" data-aria-posinset="3" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Simple scaling. Redis handles thousands of concurrent consumers trivially.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;The trade-off is durability. Redis can persist&amp;nbsp;to&amp;nbsp;disk, but&amp;nbsp;it’s&amp;nbsp;not ACID. If Redis crashes between a pop and the job completing, you might lose or duplicate work (though Redis Streams with consumer groups mitigate this significantly). For most job queue use cases, at-least-once delivery is acceptable, and Redis does that well.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3 aria-level="2"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;Kafka&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;For truly high-throughput, distributed workloads, Apache Kafka is the heavyweight&amp;nbsp;option. Kafka partitions give you parallel consumption with ordering guarantees per partition, durable storage, and replay capability.&amp;nbsp;It’s&amp;nbsp;the right tool when:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="1" data-list-defn-props="{" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;You need to process&amp;nbsp;thousands&amp;nbsp;of events per second&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="1" data-list-defn-props="{" data-aria-posinset="2" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Multiple consumers need to read the same events&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="1" data-list-defn-props="{" data-aria-posinset="3" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;You want event replay or audit trails&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="1" data-list-defn-props="{" data-aria-posinset="4" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Your architecture is already event-driven&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;The operational overhead is nontrivial –&amp;nbsp;ZooKeeper&amp;nbsp;(or&amp;nbsp;KRaft), brokers, topic management, consumer&amp;nbsp;group coordination. But for teams already running Kafka for other reasons, adding a job queue topic is&amp;nbsp;practically free.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2 aria-level="1"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;Choosing the Right Tool&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Here’s&amp;nbsp;a rough decision guide:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="4" data-list-defn-props="{" data-aria-posinset="1" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Under 100 concurrent workers, simple jobs, Postgres with &lt;CODE&gt;SKIP LOCKED&lt;/CODE&gt; is fine&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="4" data-list-defn-props="{" data-aria-posinset="2" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Moderate concurrency, want to stay in Postgres, Advisory locks or&amp;nbsp;pgq&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="4" data-list-defn-props="{" data-aria-posinset="3" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;High throughput, low-latency dispatch, Redis (Lists or Streams)&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="4" data-list-defn-props="{" data-aria-posinset="4" data-aria-level="1"&gt;&lt;SPAN data-contrast="auto"&gt;Massive scale, distributed, event replay, Kafka&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Many teams that start with Postgres (reasonably) hit scaling problems and then try to fix Postgres rather than recognizing that the workload has outgrown the tool. They throw more autovacuum workers at it, increase &lt;CODE&gt;max_connections&lt;/CODE&gt;, add connection poolers – all of which help at the margins, but don’t address the fundamental issue: Postgres’s MVCC and locking machinery wasn’t designed for this access pattern at high concurrency.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2 aria-level="1"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 1"&gt;Conclusion&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Postgres is great, but it&amp;nbsp;can’t&amp;nbsp;be the best tool for every job. Using it as a job queue is a perfectly valid choice when your scale is modest. But when&amp;nbsp;you’re&amp;nbsp;running thousands of concurrent workers, the combination of&amp;nbsp;MultiXact&amp;nbsp;SLRU contention, heap bloat, vacuum pressure, and raw locking overhead will eventually push you toward a purpose-built solution.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;The good news is that you&amp;nbsp;don’t&amp;nbsp;have to rip out everything. Advisory locks can buy&amp;nbsp;you&amp;nbsp;headroom without adding infrastructure. Redis can handle dispatch while Postgres keeps owning the data. And if&amp;nbsp;you’re&amp;nbsp;already&amp;nbsp;using&amp;nbsp;Kafka, a job topic is a natural fit.&amp;nbsp;&amp;nbsp;Take your pick – there are many&amp;nbsp;queueing&amp;nbsp;options out there!&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;</description>
      <pubDate>Thu, 30 Apr 2026 15:10:50 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/potential-consequences-of-using-postgres-as-a-job-queue/ba-p/4514332</guid>
      <dc:creator>richyen</dc:creator>
      <dc:date>2026-04-30T15:10:50Z</dc:date>
    </item>
    <item>
      <title>Connection Scaling in Elastic Clusters</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/connection-scaling-in-elastic-clusters/ba-p/4509624</link>
      <description>&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;&lt;SPAN data-ccp-charstyle="Normal"&gt;As your applications grow, you need to manage database connections carefully to keep performance predictable and reliable. Azure Database for PostgreSQL Elastic Clusters, powered by Citus, support horizontal scaling by distributing data and queries across multiple nodes. &lt;/SPAN&gt;&lt;/SPAN&gt;This raises a key question: how do connections behave as you add more clients, grow the cluster, or upgrade node specifications?&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;In this post, you’ll see how connection handling behaves in Elastic Clusters &lt;SPAN data-contrast="auto"&gt;through controlled benchmarks across a small set of configurations.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;201341983&amp;quot;:0,&amp;quot;335551550&amp;quot;:1,&amp;quot;335551620&amp;quot;:1,&amp;quot;335559685&amp;quot;:0,&amp;quot;335559737&amp;quot;:0,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:160,&amp;quot;335559740&amp;quot;:279}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Core count: 2 and 4&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Cluster node count: 4 and 8&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Using these setups, we measured how throughput, latency, and resource usage change as connection counts increase for different workloads. The goal is not just to show numbers, but to explain why those numbers behave the way they do. We deliberately chose small SKUs (2 cores/8 GB and 4 cores/32 GB) because it is easier to observe how a smaller compute responds as connection counts grow.&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;These results help you understand:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;
&lt;P&gt;What influences connection capacity&lt;/P&gt;
&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;When scaling up helps more than scaling out&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;How to tune your cluster to match real workloads&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Future posts will expand this analysis to more configurations.&amp;nbsp;For now, these insights&amp;nbsp;can&amp;nbsp;help you make informed decisions about cluster sizing and connection management.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;201341983&amp;quot;:0,&amp;quot;335551550&amp;quot;:1,&amp;quot;335551620&amp;quot;:1,&amp;quot;335559685&amp;quot;:0,&amp;quot;335559737&amp;quot;:0,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:160,&amp;quot;335559740&amp;quot;:279}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;What&amp;nbsp;you'll&amp;nbsp;learn:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;How &lt;STRONG&gt;single-shard&lt;/STRONG&gt; and &lt;STRONG&gt;multi-shard&lt;/STRONG&gt; queries behave under increasing connection loads&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;The impact of &lt;STRONG&gt;scaling up&lt;/STRONG&gt; (more cores per node) vs. &lt;STRONG&gt;scaling out&lt;/STRONG&gt; (more nodes)&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;When&amp;nbsp;&lt;STRONG&gt;PgBouncer&lt;/STRONG&gt;&amp;nbsp;helps—and when it hurts&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Practical limits imposed by memory, CPU, and connection parameters&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Configuration guidelines for different workload patterns&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;How Elastic Clusters Handle Client and Internal Connections&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Before we dive into the performance results,&amp;nbsp;let’s&amp;nbsp;first look at how Elastic Clusters handle connections and execute queries.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3 aria-level="3"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-charstyle="Normal"&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt;Cluster Architecture: Where Connections Go&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;In Elastic Clusters, data is distributed across multiple nodes using&amp;nbsp;Citus. Each node can accept client connections and execute queries. This is true&amp;nbsp;even&amp;nbsp;when&amp;nbsp;the data lives on another node. This design allows you to scale horizontally while staying compatible with PostgreSQL.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;When your application opens a connection, the flow looks like this:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;The client connects through &lt;STRONG&gt;a load balancer&lt;/STRONG&gt; on port &lt;STRONG&gt;7432&lt;/STRONG&gt;&lt;/SPAN&gt;&lt;STRONG&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;The load balancer routes the connection to &lt;STRONG&gt;any available node&lt;/STRONG&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;That node executes the query and talks to other nodes if needed&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;From the client’s point of view, this looks like a single server. &lt;/SPAN&gt;Behind the scenes, however, the node can open additional internal connections to efficiently fetch distributed data.&lt;/P&gt;
&lt;H3 aria-level="3"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt;Query Types&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt;Determine&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt;&amp;nbsp;Connection&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt;Use&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;How many connections a query consumes depends on what kind of query you run.&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;img /&gt;
&lt;P class="lia-clear-both"&gt;&amp;nbsp;&lt;/P&gt;
&lt;H4&gt;&lt;SPAN data-contrast="auto"&gt;Single-Shard Queries&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H4&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Single-shard queries target &lt;STRONG&gt;data that lives on exactly one node&lt;/STRONG&gt;. A typical example is a lookup&amp;nbsp;by&amp;nbsp;the distribution key.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;If you connect to the node that owns the data, the query runs locally&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;If you connect to a different node, Citus uses an internal connection to fetch the data&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="lia-clear-both"&gt;&amp;nbsp;&lt;/P&gt;
&lt;H4&gt;&lt;SPAN data-contrast="auto"&gt;Multi-Shard (Fan-Out) Queries&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H4&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Multi-shard queries need &lt;STRONG&gt;data from multiple nodes&lt;/STRONG&gt;. Aggregations and analytical queries often fall into this category. For these queries:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;The connected node has internal connections to many nodes&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Each node processes its local shards in parallel&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Results are sent back and combined&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;As a result,&amp;nbsp;one&amp;nbsp;client connection can &lt;STRONG&gt;fan out into many internal connections.&lt;/STRONG&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;In both cases, when&amp;nbsp;Citus&amp;nbsp;needs to fetch data from another node, it first&amp;nbsp;attempts&amp;nbsp;to reuse a &lt;STRONG&gt;cached internal&amp;nbsp;connection&lt;/STRONG&gt;. If&amp;nbsp;none is available,&amp;nbsp;it creates&amp;nbsp;a&amp;nbsp;new connection&amp;nbsp;and caches it.&amp;nbsp;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Important note:&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt; &lt;/STRONG&gt;Explicit transactions change the behavior of single-shard queries. When a query runs inside a BEGIN … END block, Citus sends extra commands (BEGIN TRANSACTION + assign ID, COMMIT) to the worker node alongside the actual query. &lt;/SPAN&gt;These extra network roundtrips add some coordination work compared to auto‑commit mode.&lt;/P&gt;
&lt;H4&gt;&lt;SPAN data-ccp-props="{}"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt;Key Configuration Parameters&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/H4&gt;
&lt;P&gt;&lt;SPAN data-ccp-props="{}"&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&lt;SPAN data-contrast="auto"&gt;Elastic Clusters expose configuration parameters that control how connections are created, cached, and limited. To understand connection scaling—or to tune it effectively—you need to know what these parameters do and when they matter. This section focuses on a subset; see the Citus blog posts and documentation for more details.&lt;/SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table border="1" style="width: 100%; height: 250px; border-width: 1px;"&gt;&lt;tbody&gt;&lt;tr style="height: 34.8px;"&gt;&lt;td style="height: 34.8px;"&gt;&lt;STRONG&gt;Parameter&lt;/STRONG&gt;&lt;/td&gt;&lt;td style="height: 34.8px;"&gt;&lt;STRONG&gt;Scope&lt;/STRONG&gt;&lt;/td&gt;&lt;td style="height: 34.8px;"&gt;&lt;STRONG&gt;Description&lt;/STRONG&gt;&lt;/td&gt;&lt;td style="height: 34.8px;"&gt;&lt;STRONG&gt;Typical use case&lt;/STRONG&gt;&lt;/td&gt;&lt;td style="height: 34.8px;"&gt;&lt;STRONG&gt;Default&lt;/STRONG&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr style="height: 58.8px;"&gt;&lt;td style="height: 58.8px;"&gt;max_connections&lt;/td&gt;&lt;td style="height: 58.8px;"&gt;Cluster wide&lt;/td&gt;&lt;td style="height: 58.8px;"&gt;Total connections allowed&lt;/td&gt;&lt;td style="height: 58.8px;"&gt;Set based on resources&lt;/td&gt;&lt;td style="height: 58.8px;"&gt;Varies by SKU&lt;/td&gt;&lt;/tr&gt;&lt;tr style="height: 38.8px;"&gt;&lt;td style="height: 38.8px;"&gt;
&lt;P&gt;citus.max_client_connections&lt;/P&gt;
&lt;/td&gt;&lt;td style="height: 38.8px;"&gt;Per-node&lt;/td&gt;&lt;td style="height: 38.8px;"&gt;Connections allowed from clients&lt;/td&gt;&lt;td style="height: 38.8px;"&gt;Limit client load per node&lt;/td&gt;&lt;td style="height: 38.8px;"&gt;Varies by SKU&lt;/td&gt;&lt;/tr&gt;&lt;tr style="height: 58.8px;"&gt;&lt;td style="height: 58.8px;"&gt;&lt;SPAN data-contrast="auto"&gt;citus.max_cached_conns_per_worker&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/td&gt;&lt;td style="height: 58.8px;"&gt;Per client connection&lt;/td&gt;&lt;td style="height: 58.8px;"&gt;Cached internal connections&lt;/td&gt;&lt;td style="height: 58.8px;"&gt;Control parallelism in fan-out queries&lt;/td&gt;&lt;td style="height: 58.8px;"&gt;1&lt;/td&gt;&lt;/tr&gt;&lt;tr style="height: 58.8px;"&gt;&lt;td style="height: 58.8px;"&gt;&lt;SPAN data-contrast="auto"&gt;citus.max_adaptive_executor_pool_size&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/td&gt;&lt;td style="height: 58.8px;"&gt;Per client connection&lt;/td&gt;&lt;td style="height: 58.8px;"&gt;Max connections for parallel multi-shard execution&lt;/td&gt;&lt;td style="height: 58.8px;"&gt;Control parallelism in fan-out queries&lt;/td&gt;&lt;td style="height: 58.8px;"&gt;Varies by SKU, 1 or 16&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 20.00%" /&gt;&lt;col style="width: 20.00%" /&gt;&lt;col style="width: 20.00%" /&gt;&lt;col style="width: 20.00%" /&gt;&lt;col style="width: 20.00%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;H5&gt;&lt;SPAN data-contrast="auto"&gt;&lt;SPAN data-contrast="none"&gt;In PostgreSQL, Memory Plays a Key Role in Connection Scaling&lt;/SPAN&gt;:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H5&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Idle connections typically use 2–5 MB&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Active connections often use 10–20 MB, depending on&amp;nbsp;work_mem&amp;nbsp;and query behavior&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H5&gt;&lt;SPAN data-contrast="auto"&gt;Some Parameters Only Affect Certain Query Types:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H5&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Parameters like&amp;nbsp;&lt;EM&gt;citus.max_adaptive_executor_pool_size&lt;/EM&gt;&amp;nbsp;only affect multi-shard queries.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H5&gt;&lt;SPAN data-contrast="auto"&gt;Internal Connection Caching Is Critic&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;al:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H5&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Internal connection caching has&amp;nbsp;a large impact&amp;nbsp;on performance.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Setting&lt;EM&gt;&amp;nbsp;citus.max_cached_conns_per_worker&lt;/EM&gt;&amp;nbsp;to 0 forces&amp;nbsp;Citus&amp;nbsp;to open new connections repeatedly&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;
&lt;P&gt;This leads to additional connection setup work and reduces overall throughput&lt;/P&gt;
&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Values greater than 1 only help multi-shard workloads by allowing more parallelism&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;In practice, disabling internal caching is&amp;nbsp;usually a&amp;nbsp;bad&amp;nbsp;idea.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;201341983&amp;quot;:0,&amp;quot;335551550&amp;quot;:1,&amp;quot;335551620&amp;quot;:1,&amp;quot;335559685&amp;quot;:0,&amp;quot;335559737&amp;quot;:0,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:160,&amp;quot;335559740&amp;quot;:279}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H5&gt;&lt;SPAN data-contrast="auto"&gt;SKU Choice Changes Which&amp;nbsp;Limits&amp;nbsp;You Hit First&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H5&gt;
&lt;UL&gt;
&lt;LI&gt;
&lt;P&gt;Smaller nodes tend to reach memory thresholds sooner&lt;/P&gt;
&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Larger nodes may hit connection limits before CPU or memory saturation&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2 aria-level="2"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;How We Benchmarked&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;To understand how connections scale in Elastic Clusters, we ran a set of controlled benchmarks using standard PostgreSQL tooling.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3 aria-level="3"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt;Test Environment&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="16" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="1" data-aria-level="1"&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Tool&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;:&amp;nbsp;&lt;/STRONG&gt;pgbench (PostgreSQL's standard benchmarking utility), running on a VM in the same region and zone as the cluster &lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;UL&gt;
&lt;LI aria-setsize="-1" data-leveltext="" data-font="Symbol" data-listid="16" data-list-defn-props="{&amp;quot;335552541&amp;quot;:1,&amp;quot;335559685&amp;quot;:720,&amp;quot;335559991&amp;quot;:360,&amp;quot;469769226&amp;quot;:&amp;quot;Symbol&amp;quot;,&amp;quot;469769242&amp;quot;:[8226],&amp;quot;469777803&amp;quot;:&amp;quot;left&amp;quot;,&amp;quot;469777804&amp;quot;:&amp;quot;&amp;quot;,&amp;quot;469777815&amp;quot;:&amp;quot;hybridMultilevel&amp;quot;}" data-aria-posinset="2" data-aria-level="1"&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Dataset&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;: &lt;/STRONG&gt;Distributed&amp;nbsp;pgbench_accounts&amp;nbsp;table&amp;nbsp;with a scale factor&amp;nbsp;of&amp;nbsp;&amp;nbsp;3000&amp;nbsp;(~38 GB)&amp;nbsp;and distribution column aid (account ID).&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Setup commands:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="bash"&gt;# Create table structure pgbench -i -I "dt" # Distribute the accounts table psql -c "SELECT create_distributed_table('pgbench_accounts', 'aid');" # Populate with data pgbench -i -I "gvp" -s 3000 # Add index for multi-shard queries CREATE INDEX index_bid ON pgbench_accounts(bid);&lt;/LI-CODE&gt;
&lt;H3&gt;Workloads&amp;nbsp;&lt;/H3&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Single-Shard Query&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;:&lt;/STRONG&gt;&amp;nbsp;The single-shard workload targets one shard using the distribution key.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;\set aid random(1, 100000 * scale) BEGIN; SELECT abalance FROM pgbench_accounts WHERE aid = :aid; END;&lt;/LI-CODE&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Multi-Shard Query:&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;&amp;nbsp;&lt;/STRONG&gt;The multi-shard workload aggregates data across multiple shards.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;\set bid random(1, scale) BEGIN; SELECT sum(abalance) FROM pgbench_accounts WHERE bid = :bid; END;&lt;/LI-CODE&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;How the Benchmarks Run&lt;/SPAN&gt;&lt;SPAN data-contrast="auto"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Each benchmark:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Ran for&amp;nbsp;600 seconds&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Used&amp;nbsp;16&amp;nbsp;pgbench&amp;nbsp;worker threads&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Varied the number of client connections (-c) per run&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;For&amp;nbsp;each&amp;nbsp;cluster configuration, we gradually increased the client count. We stopped when we&amp;nbsp;observed&amp;nbsp;connection&amp;nbsp;or&amp;nbsp;out-of-memory&amp;nbsp;errors.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;201341983&amp;quot;:0,&amp;quot;335551550&amp;quot;:1,&amp;quot;335551620&amp;quot;:1,&amp;quot;335559685&amp;quot;:0,&amp;quot;335559737&amp;quot;:0,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:160,&amp;quot;335559740&amp;quot;:279}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;201341983&amp;quot;:0,&amp;quot;335551550&amp;quot;:1,&amp;quot;335551620&amp;quot;:1,&amp;quot;335559685&amp;quot;:0,&amp;quot;335559737&amp;quot;:0,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:160,&amp;quot;335559740&amp;quot;:279}"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt;Test Matrix&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;P&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;201341983&amp;quot;:0,&amp;quot;335551550&amp;quot;:1,&amp;quot;335551620&amp;quot;:1,&amp;quot;335559685&amp;quot;:0,&amp;quot;335559737&amp;quot;:0,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:160,&amp;quot;335559740&amp;quot;:279}"&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&lt;SPAN data-contrast="auto"&gt;We evaluated combinations of core counts, node counts, and query types:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table border="1" style="width: 70.1852%; border-width: 1px;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;STRONG&gt;Workload&lt;/STRONG&gt;&lt;/td&gt;&lt;td&gt;&lt;STRONG&gt;Configuration tested&lt;/STRONG&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;SPAN data-contrast="auto"&gt;Single-shard&amp;nbsp;&lt;/SPAN&gt;&lt;/td&gt;&lt;td&gt;&lt;SPAN data-contrast="auto"&gt;2-core/4-node, 2-core/8-node, 4-core/4-node, 4-core/8-node&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;SPAN data-contrast="auto"&gt;Multi-shard&amp;nbsp;&lt;/SPAN&gt;&lt;/td&gt;&lt;td&gt;&lt;SPAN data-contrast="auto"&gt;2-core/4-node, 2-core/8-node, 4-core/4-node, 4-core/8-node&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;SPAN data-contrast="auto"&gt;Single-shard with PgBouncer&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/td&gt;&lt;td&gt;&lt;SPAN data-contrast="auto"&gt;2-core/4-node, 2-core/8-node&lt;/SPAN&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 50.00%" /&gt;&lt;col style="width: 50.00%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;H2&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;Single-Shard Query Performance&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&lt;SPAN data-contrast="auto"&gt;Single&lt;/SPAN&gt;‑&lt;SPAN data-contrast="auto"&gt;shard queries are a good model for OLTP workloads. They&amp;nbsp;represent&amp;nbsp;indexed lookups that target a single row (or&amp;nbsp;a small&amp;nbsp;set of rows).&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table border="1" style="width: 100%; border-width: 1px;"&gt;&lt;colgroup&gt;&lt;col style="width: 50.0371%" /&gt;&lt;col style="width: 50.0371%" /&gt;&lt;/colgroup&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class="lia-align-center" colspan="2"&gt;
&lt;P&gt;&lt;SPAN class="lia-text-color-15"&gt;&lt;STRONG&gt;Massive Throughput Gains with Scaling&amp;nbsp;&lt;/STRONG&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="none"&gt;Peak throughput climbed from&lt;STRONG&gt; ~&lt;/STRONG&gt;&lt;/SPAN&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="none"&gt;11.4k TPS&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="none"&gt;&lt;STRONG&gt;&amp;nbsp;(2c4n)&lt;/STRONG&gt; to &lt;STRONG&gt;~&lt;/STRONG&gt;&lt;/SPAN&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="none"&gt;48.3k TPS&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="none"&gt;&lt;STRONG&gt;&amp;nbsp;(4c8n)&lt;/STRONG&gt;, a&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-contrast="none"&gt;&lt;STRONG&gt;4.3×&lt;/STRONG&gt; increase&lt;/SPAN&gt;&lt;SPAN data-contrast="none"&gt;&amp;nbsp;with quadruple cores and double the nodes.&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="lia-align-center"&gt;
&lt;P&gt;&lt;SPAN class="lia-text-color-15"&gt;&lt;STRONG&gt;Higher Concurrency = Saturation Point&amp;nbsp;&lt;/STRONG&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;Each configuration has an optimal operating point for throughput.&lt;/P&gt;
&lt;/td&gt;&lt;td class="lia-align-center"&gt;
&lt;P&gt;&lt;SPAN class="lia-text-color-15"&gt;&lt;STRONG&gt;Near-Linear Scaling&amp;nbsp;&lt;/STRONG&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="none"&gt;Doubling cores&amp;nbsp;roughly doubled&amp;nbsp;throughput (≈2.4–2.5× gains), and doubling nodes added ~70–75% more TPS.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;335551550&amp;quot;:2,&amp;quot;335551620&amp;quot;:2}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN lia-align-center"&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN lia-align-center"&gt;&lt;img /&gt;
&lt;P class="lia-clear-both"&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;&lt;/DIV&gt;
&lt;H3&gt;Key Takeaways from Single-shard Experiments&lt;/H3&gt;
&lt;H4&gt;1. &lt;SPAN data-contrast="auto"&gt;Near-Linear Scaling with Resources&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H4&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Doubling CPU cores&amp;nbsp;approximately doubled&amp;nbsp;throughput, and doubling node count added 70-75% more throughput:&lt;/SPAN&gt;&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table class="lia-border-style-solid" border="1" style="width: 51.3889%; height: 186px; border-width: 1px;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Configuration&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Peak TPS&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Peak Clients&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;CPU at Peak&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;2-core, 4-node&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;11,400&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;64&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;~95%&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;2-core, 8-node&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;19,400&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;192&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;~93%&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;4-core, 4-node&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;27,500&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;128&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;~97%&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;4-core, 8-node&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;48,300&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;224&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;~90%&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 25.00%" /&gt;&lt;col style="width: 25.00%" /&gt;&lt;col style="width: 25.00%" /&gt;&lt;col style="width: 25.00%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;What this means&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;: &lt;/STRONG&gt;Adding compute resources effectively increases capacity. The slight sub-linearity when adding nodes (1.7× instead of 2×) is because the extra round-trips from explicit transactions only affect remote shard queries and the fraction for remote queries grows with the cluster size. In a 4-node cluster, 75% of queries hit a remote shard; in a 8-node cluster, 87.5% do. Since each remote query inside a BEGIN … END block pays the extra round-trip cost, the aggregate overhead increases as more nodes are added.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H4&gt;&lt;SPAN data-contrast="auto"&gt;2.Saturation Points Vary by Configuration&lt;/SPAN&gt;&lt;/H4&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Each cluster configuration reaches &lt;STRONG&gt;peak throughput at a specific client count&lt;/STRONG&gt;. Beyond this point,&amp;nbsp;additional&amp;nbsp;clients&amp;nbsp;lead to:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134233117&amp;quot;:false,&amp;quot;134233118&amp;quot;:false,&amp;quot;201341983&amp;quot;:0,&amp;quot;335551550&amp;quot;:1,&amp;quot;335551620&amp;quot;:1,&amp;quot;335559685&amp;quot;:0,&amp;quot;335559737&amp;quot;:0,&amp;quot;335559738&amp;quot;:0,&amp;quot;335559739&amp;quot;:160,&amp;quot;335559740&amp;quot;:279}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Throughput plateau&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;
&lt;P&gt;Latency rises as the system operates near maximum capacity&lt;/P&gt;
&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Increased contention and context switching&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Smaller configurations reach peak throughput sooner&lt;SPAN data-contrast="auto"&gt; (64 clients for 2c4n), while larger ones handle several hundred concurrent clients before reaching peak.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H4&gt;&lt;SPAN style="color: rgb(30, 30, 30);"&gt;3. Memory Constrains Maximum Connections, Not Node Count&lt;/SPAN&gt;&lt;/H4&gt;
&lt;P&gt;A key insight is that &lt;STRONG&gt;adding nodes does not &lt;SPAN data-contrast="auto"&gt;necessarily &lt;/SPAN&gt;increase total client capacity when the workload is constrained by memory limits&lt;/STRONG&gt;.&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;2-core clusters: Maximum ~500 concurrent clients (both 4-node and 8-node)&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;4-core clusters: Maximum ~1000 concurrent clients (both 4-node and 8-node)&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Why?&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&amp;nbsp;Each node&amp;nbsp;maintains&amp;nbsp;roughly&amp;nbsp;client_count&amp;nbsp;total connections:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;client_count&amp;nbsp;/&amp;nbsp;node_count&amp;nbsp;external client connections (via load balancer)&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Remaining connections are cached internal&amp;nbsp;Citus&amp;nbsp;connections&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Once memory capacity is fully utilized, adding nodes alone does not always raise client capacity. External connections drop per node, but internal connections replace them, so each node still carries roughly the same load—and still can run out of memory.&lt;/P&gt;
&lt;H4&gt;&lt;SPAN style="color: rgb(30, 30, 30);"&gt;4. CPU Utilization Reaches 90-97% at Peak&lt;/SPAN&gt;&lt;/H4&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;All configurations fully&amp;nbsp;utilized&amp;nbsp;available&amp;nbsp;CPU at peak throughput, confirming CPU as the primary throughput bottleneck (with memory limiting connection capacity separately).&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H4 aria-level="3"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 3"&gt;5. Latency Characteristics&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H4&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Latency&amp;nbsp;remains&amp;nbsp;low (&amp;lt;5-10ms) until the system approaches saturation. Larger clusters&amp;nbsp;maintain better latency&amp;nbsp;under load:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;4c8n cluster&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;:&lt;/STRONG&gt; Sub-5ms latency up to ~200 clients&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;2c4n cluster&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;: &lt;/STRONG&gt;Latency exceeds 10-20ms once saturated (~64 clients)&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Practical implication&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;:&amp;nbsp;&lt;/STRONG&gt;Right-sizing&amp;nbsp;your cluster provides headroom for traffic spikes without latency degradation.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2 aria-level="2"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;Single-Shard Query Performance wit&lt;/SPAN&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;h PgBouncer&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;On Elastic Clusters, you can enable PgBouncer using server parameters. After enabling it, you connect to PgBouncer instances through the load balancer &lt;STRONG&gt;on port 8432&lt;/STRONG&gt;. This connection pooling layer allows the cluster to handle far more concurrent client connections&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;PgBouncer&amp;nbsp;instances&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;: &lt;/STRONG&gt;One per node, behind load balancer&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Pool size&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;: &lt;/STRONG&gt;50 connections per node (default)&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Pooling mode&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;: &lt;/STRONG&gt;Transaction pooling&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;table class="lia-border-style-solid" border="1" style="width: 93.5185%; height: 181.6px; border-width: 1px;"&gt;&lt;tbody&gt;&lt;tr style="height: 104.8px;"&gt;&lt;td class="lia-align-center" style="height: 104.8px;"&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN class="lia-text-color-15"&gt;Eliminated&amp;nbsp;Connection Limits&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="none"&gt;Handled thousands of concurrent clients without out-of-memory errors&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td class="lia-align-center" style="height: 104.8px;"&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN class="lia-text-color-15"&gt;Slightly Lower Peak TPS&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="none"&gt;~10% reduction in&amp;nbsp;maximum&amp;nbsp;throughput due to pooling overhead&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr style="height: 76.8px;"&gt;&lt;td class="lia-align-center" colspan="2" style="height: 76.8px;"&gt;
&lt;P&gt;&lt;SPAN class="lia-text-color-15"&gt;&lt;STRONG&gt;Linear Latency Growth&amp;nbsp;&lt;/STRONG&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="none"&gt;Predictable queuing behavior once pool saturates&amp;nbsp;&lt;/SPAN&gt; &lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 50.00%" /&gt;&lt;col style="width: 50.00%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;img /&gt;&lt;img /&gt;&lt;/DIV&gt;
&lt;H3&gt;Key Takeaways from Single-shard Experiments with PgBouncer&lt;/H3&gt;
&lt;H4&gt;1. &lt;SPAN data-contrast="auto"&gt;Graceful handling of high connection counts&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt; &lt;/H4&gt;
&lt;P&gt;Without PgBouncer, 2‑core clusters reached memory capacity around 500 clients. &lt;SPAN data-ccp-props="{}"&gt;&lt;SPAN data-contrast="auto"&gt;With&amp;nbsp;PgBouncer&amp;nbsp;enabled, tests successfully ran with 1000+ concurrent clients. Throughput plateaued as the pool saturated, but the system remained stable.&lt;/SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H4&gt;2. &lt;SPAN data-ccp-props="{}"&gt;&lt;SPAN data-contrast="auto"&gt;Throughput-Latency Trade-Off&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/H4&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Once the connection pool fills (~50 active connections per node),&amp;nbsp;additional&amp;nbsp;clients queue:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Throughput stabilizes at the pool's processing capacity&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;
&lt;P&gt;Latency increases with queue depth&lt;/P&gt;
&lt;/LI&gt;
&lt;LI&gt;
&lt;P&gt;Predictable, graceful behavior under high load&lt;/P&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;H4&gt;3.&amp;nbsp;&lt;SPAN data-ccp-props="{}"&gt;&lt;SPAN data-contrast="auto"&gt;When to use PgBouncer&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/H4&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Recommended for:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Applications with&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;bursty connection patterns&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&amp;nbsp;(many short-lived connections)&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;High connection counts&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&amp;nbsp;that exceed node memory capacity&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Workloads where&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;occasional queuing latency&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&amp;nbsp;is acceptable&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Not recommended for:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Applications requiring&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;maximum&amp;nbsp;throughput&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&amp;nbsp;from steady workloads&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Long-running transactions (incompatible with transaction pooling)&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;
&lt;P&gt;Scenarios where &lt;STRONG&gt;every millisecond of latency matters&lt;/STRONG&gt;&lt;/P&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;&lt;SPAN data-ccp-props="{}"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;Multi-Shard Query Performance&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-ccp-props="{}"&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&lt;SPAN data-contrast="auto"&gt;Multi-shard (fan-out) queries aggregate or join data across multiple nodes,&amp;nbsp;representing&amp;nbsp;analytical or reporting workloads.&lt;/SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table class="lia-border-style-solid" border="1" style="border-width: 1px;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class="lia-align-center"&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN class="lia-text-color-15"&gt;Massive Throughput Gains with Scaling&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="none"&gt;Scaling up and out dramatically improved fan-out query throughput – from ~52 TPS to ~1008 TPS on the largest&lt;/SPAN&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="none"&gt;&amp;nbsp;(≈20× gain)&lt;/SPAN&gt;&lt;SPAN data-contrast="none"&gt;.&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-ccp-props="{&amp;quot;335551550&amp;quot;:2,&amp;quot;335551620&amp;quot;:2}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td class="lia-align-center"&gt;
&lt;P&gt;&amp;nbsp;&lt;STRONG&gt;&lt;SPAN class="lia-text-color-15"&gt;Low Concurrency&amp;nbsp;Saturation&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="none"&gt;Multi-shard queries peaked at low client counts—just 8 clients for 2-core clusters and ~96 for 4-core, 8-node setups.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;335551550&amp;quot;:2,&amp;quot;335551620&amp;quot;:2}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="lia-align-center"&gt;
&lt;P&gt;&amp;nbsp;&lt;SPAN class="lia-text-color-15"&gt;&lt;STRONG&gt;Latency Improves with Scale&amp;nbsp;&lt;/STRONG&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="none"&gt;Larger clusters&amp;nbsp;maintained&amp;nbsp;sub-100 ms&amp;nbsp;latency under higher concurrency, while smaller ones degraded quickly.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;335551550&amp;quot;:2,&amp;quot;335551620&amp;quot;:2}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td class="lia-align-center"&gt;
&lt;P aria-level="4"&gt;&lt;STRONG&gt;&lt;SPAN class="lia-text-color-15"&gt;Memory &amp;amp; I/O Bottlenecks&amp;nbsp;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="none"&gt;&lt;STRONG&gt;Sufficient memory is crucial&lt;/STRONG&gt; for fan-out queries, as memory starvation causes throughput to plateau well before CPU is fully&amp;nbsp;utilized.&lt;/SPAN&gt; &lt;SPAN data-ccp-props="{&amp;quot;335551550&amp;quot;:2,&amp;quot;335551620&amp;quot;:2}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 50.00%" /&gt;&lt;col style="width: 50.00%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;img /&gt;&lt;img /&gt;
&lt;H3&gt;Key Takeaways from Multi-shard Experiments&lt;/H3&gt;
&lt;H4&gt;1. Lower Absolute Throughput Compared to Single-shard Workloads&lt;/H4&gt;
&lt;P&gt;&lt;SPAN data-ccp-props="{}"&gt;&lt;SPAN data-contrast="auto"&gt;Even the best-performing configuration (4c8n at ~1000 TPS) achieves ~2% of single-shard throughput (~48k TPS). This reflects the inherent complexity of the analytical fan-out queries: cross-node data aggregation, significantly large amount of data retrieval.&lt;/SPAN&gt; &amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H4&gt;&lt;SPAN data-ccp-props="{}"&gt;&lt;SPAN data-contrast="auto"&gt;&amp;nbsp;2. Scaling Provides Dramatic Gains&lt;/SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H4&gt;
&lt;P&gt;&lt;SPAN data-ccp-props="{}"&gt;&lt;SPAN data-contrast="auto"&gt;While absolute TPS&amp;nbsp;remains&amp;nbsp;modest, the&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;20× improvement from smallest to largest&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&amp;nbsp;demonstrates&amp;nbsp;that multi-shard workloads benefit enormously from scaling.&lt;/SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table class="lia-border-style-solid" border="1" style="border-width: 1px;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Configuration&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Peak TPS&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Peak Clients&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Latency at Peak&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;2-core, 4-node&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;52&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;8&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;~150ms&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;2-core, 8-node&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;168&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;8&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;~50ms&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;4-core, 4-node&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;489&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;32&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;~65ms&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;4-core, 8-node&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;1,008&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;96&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;~95ms&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 25.00%" /&gt;&lt;col style="width: 25.00%" /&gt;&lt;col style="width: 25.00%" /&gt;&lt;col style="width: 25.00%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;H4&gt;&lt;SPAN data-contrast="auto"&gt;3. Saturates at Low Concurrency&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H4&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Multi-shard queries reach peak throughput at fewer concurrent clients than single-shard queries:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;2-core clusters: Saturate at just 8 clients&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;4-core, 8-node: Saturates around 96 clients&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;After these points, the system maintains stable TPS, with latency rising as load increases.&lt;/P&gt;
&lt;H4&gt;&lt;SPAN style="color: rgb(30, 30, 30);"&gt;4. Memory and I/O Bottlenecks Dominate Small Configurations&lt;/SPAN&gt;&lt;/H4&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;The 2-core configurations (8 GB RAM per node) &lt;/SPAN&gt;showed clear resource pressure:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Memory pressure&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;: &lt;/STRONG&gt;Working sets exceeded available RAM, causing paging&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;High IOPS&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;: &lt;/STRONG&gt;Thousands of disk operations per second&amp;nbsp;indicated&amp;nbsp;swapping to disk&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Throughput ceiling&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;: &lt;/STRONG&gt;&lt;SPAN data-contrast="none"&gt;Memory availability capped TPS before CPU was fully&amp;nbsp;utilized&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;In contrast, 4-core configurations (32 GB RAM per node) kept working sets in memory, achieving much higher throughput with minimal I/O.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Key insight&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;: &lt;/STRONG&gt;For multi-shard workloads,&amp;nbsp;&lt;/SPAN&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;sufficient memory is more important than CPU cores&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;. &lt;SPAN data-contrast="none"&gt;Adequate memory provisioning is essential to unlock full&amp;nbsp;performance&lt;/SPAN&gt;.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H4&gt;&lt;SPAN style="color: rgb(30, 30, 30);"&gt;5. Latency Escalates Rapidly Under Overload&lt;/SPAN&gt;&lt;/H4&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;All configurations&amp;nbsp;delivered&amp;nbsp;fast response times (&amp;lt;50ms) at low loads. Once saturated:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;2c4n cluster&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;:&lt;/STRONG&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&lt;SPAN data-contrast="none"&gt;Latency increased noticeably under sustained overload&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;4c8n cluster&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;:&lt;/STRONG&gt; Remained under 100ms until approaching the 96-client saturation point&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Practical implication&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;: &lt;/STRONG&gt;For multi-shard query workloads, over-provision resources to maintain consistent and predictable latency.&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H2 aria-level="2"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;Conclusion&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;Connection scaling in Azure Database for PostgreSQL Elastic Clusters is a multifaceted challenge that depends on workload characteristics, cluster configuration, and resource constraints. &lt;STRONG&gt;Key takeaways from our benchmarking on read workloads&lt;/STRONG&gt;:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;For Single-Shard OLTP-like Workloads:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Scaling provides near-linear throughput gains (4.3× from 2c4n to 4c8n)&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Memory, not node count,&amp;nbsp;determines&amp;nbsp;maximum concurrent connections&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;CPU becomes the throughput bottleneck at peak load&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;PgBouncer&amp;nbsp;trades ~10%&amp;nbsp;throughput for&amp;nbsp;almost&amp;nbsp;unlimited connection scalability&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;For Multi-Shard OLAP-like Workloads:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN data-ccp-props="{}"&gt;&lt;SPAN data-contrast="none"&gt;Throughput&amp;nbsp;operates&amp;nbsp;at a different scale than single&lt;/SPAN&gt;‑&lt;SPAN data-contrast="none"&gt;shard workloads&lt;/SPAN&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Relative gains from scaling are massive (20× improvement&amp;nbsp;observed)&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Memory sufficiency is critical—&lt;SPAN data-contrast="none"&gt;adequate RAM is essential to&amp;nbsp;maintain&amp;nbsp;strong performance&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-contrast="auto"&gt;Saturation occurs at low concurrency; keep client counts conservative&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;General Principles:&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Scale up&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&amp;nbsp;(higher SKU) to support more connections and memory-intensive queries&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Scale out&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&amp;nbsp;(more nodes) to increase aggregate throughput and data capacity&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Use&amp;nbsp;PgBouncer&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;&amp;nbsp;&lt;/STRONG&gt;to manage connection bursts and exceed node memory limits&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;SPAN data-contrast="auto"&gt;Monitor continuously&lt;/SPAN&gt;&lt;/STRONG&gt;&lt;SPAN data-contrast="auto"&gt;&lt;STRONG&gt;&amp;nbsp;&lt;/STRONG&gt;and adjust based on actual workload patterns&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;By understanding these dynamics and applying the decision frameworks provided, you can architect Elastic Clusters that deliver&amp;nbsp;optimal&amp;nbsp;performance, reliability, and cost-efficiency for your specific application requirements.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN data-contrast="auto"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3 aria-level="2"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-parastyle="heading 2"&gt;References and Resources&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;SPAN data-ccp-props="{&amp;quot;134245418&amp;quot;:true,&amp;quot;134245529&amp;quot;:true,&amp;quot;335559738&amp;quot;:160,&amp;quot;335559739&amp;quot;:80}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/H3&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;A href="https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/concepts-elastic-clusters" target="_blank" rel="noopener"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-charstyle="Hyperlink"&gt;https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/concepts-elastic-clusters&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN data-teams="true"&gt;&lt;A href="https://learn.microsoft.com/en-us/postgresql/citus" target="_blank" rel="noopener" aria-label="Link https://learn.microsoft.com/en-us/postgresql/citus"&gt;https://learn.microsoft.com/en-us/postgresql/citus&lt;/A&gt;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://www.postgresql.org/docs/current/runtime-config-connection.html" target="_blank" rel="noopener"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-charstyle="Hyperlink"&gt;https://www.postgresql.org/docs/current/runtime-config-connection.html&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://www.postgresql.org/docs/current/pgbench.html" target="_blank" rel="noopener"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-charstyle="Hyperlink"&gt;https://www.postgresql.org/docs/current/pgbench.html&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://techcommunity.microsoft.com/blog/adforpostgresql/analyzing-the-limits-of-connection-scalability-in-postgres/1757266" target="_blank" rel="noopener"&gt;&lt;SPAN data-contrast="none"&gt;&lt;SPAN data-ccp-charstyle="Hyperlink"&gt;Analyzing the Limits of Connection Scalability in Postgres | Microsoft Community Hub&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN data-ccp-props="{}"&gt;&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;</description>
      <pubDate>Wed, 22 Apr 2026 14:31:00 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/connection-scaling-in-elastic-clusters/ba-p/4509624</guid>
      <dc:creator>EbruAydin</dc:creator>
      <dc:date>2026-04-22T14:31:00Z</dc:date>
    </item>
    <item>
      <title>General Availability Refresh: Mirroring Azure Database for PostgreSQL in Microsoft Fabric</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/general-availability-refresh-mirroring-azure-database-for/ba-p/4511726</link>
      <description>&lt;H1&gt;Introduction&lt;/H1&gt;
&lt;P&gt;We are excited to announce the General Availability Refresh of Mirroring Azure Database for PostgreSQL in Microsoft Fabric. This update introduces several new capabilities designed to reduce friction, improve transparency, and increase trust in PostgreSQL data for analytics and AI workloads. Database professionals, DBAs, and developers can now leverage a more robust, flexible, and transparent solution for integrating PostgreSQL data in Microsoft Fabric.&lt;/P&gt;
&lt;P&gt;Mirroring Azure Database for PostgreSQL enables seamless integration of transactional data into Microsoft Fabric, supporting advanced analytics and AI scenarios. Previously, users faced limitations in data type support, operational requirements, and troubleshooting transparency. The General Availability Refresh directly addresses these challenges, delivering a more reliable and user-friendly experience.&lt;/P&gt;
&lt;H2&gt;Native Data Type Support&lt;/H2&gt;
&lt;P&gt;One of the most significant enhancements is support for PostgreSQL native data types, including JSON and JSONB. Users can now mirror complex, semi-structured data without conversion, preserving the full fidelity of source data. Additionally, transparent replication to &lt;EM&gt;varchar(max)&lt;/EM&gt; or &lt;EM&gt;varbinary&lt;/EM&gt; is now supported, ensuring that even custom or less common types can be accurately mirrored in Fabric.&lt;/P&gt;
&lt;P&gt;Previously, data type limitations often required workarounds or manual transformations, introducing complexity and risk. With this update, data flows more naturally from PostgreSQL to Fabric, streamlining analytics and AI pipelines and reducing the time spent on data preparation.&lt;/P&gt;
&lt;P&gt;Mirroring now preserves PostgreSQL-native types—including JSON and JSONB—without requiring schema transformations or type coercion. This ensures:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Full fidelity replication of semi-structured data&lt;/LI&gt;
&lt;LI&gt;Elimination of intermediate serialization (e.g., string encoding)&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Tables containing JSON/JSONB columns are mirrored into Fabric with their structure intact, enabling:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Direct querying of nested JSON fields&lt;/LI&gt;
&lt;LI&gt;Consistent schema representation across source and analytical layers&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Once mirrored, JSON content can be queried natively within Fabric workloads, allowing:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Hybrid analytical scenarios (relational + semi-structured)&lt;/LI&gt;
&lt;LI&gt;Use of familiar SQL patterns for JSON traversal and extraction&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;Flexible Server High Availability Support for PG versions earlier than 17&lt;/H2&gt;
&lt;P&gt;The refresh expands support for Azure Database for PostgreSQL Flexible Server with high availability enabled on versions earlier than 17. This means organizations can leverage mirroring with HA configurations without needing to upgrade their database engine, improving operational flexibility and minimizing disruption.&lt;/P&gt;
&lt;P&gt;In the past, mirroring required specific PostgreSQL versions, limiting adoption for organizations with established HA deployments. Now, the broader compatibility allows teams to maintain their preferred configurations and benefit from seamless data integration in Fabric.&lt;/P&gt;
&lt;H2&gt;Improved Transparency: Enhanced Error Messaging and Dedicated PostgreSQL UDFs&lt;/H2&gt;
&lt;P&gt;Transparency is critical for troubleshooting and maintaining trust in data pipelines. The General Availability Refresh introduces enhanced error messaging, providing clear and actionable insights into replication issues. Dedicated PostgreSQL user-defined functions (UDFs) further support monitoring and diagnostics, enabling users to quickly identify and resolve problems.&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;--Validates that all system and configuration requirements are met before starting CDC mirroring. 
SELECT * FROM azure_cdc.check_prerequisites(); 

-- Quickly verify which extension version is deployed — critical for troubleshooting 
SELECT azure_cdc.azure_cdc_version(); 

-- Returns a detailed list of errors and issues detected during CDC operations 
SELECT * FROM azure_cdc.get_health_status('', ''); 

-- Scans every eligible user table in the database and returns the mirroring readiness status for each 
SELECT * FROM azure_cdc.get_all_tables_mirror_status();&lt;/LI-CODE&gt;
&lt;P&gt;These features streamline troubleshooting, reduce downtime, and empower teams to maintain robust mirroring operations. The improvements are a substantial step forward from previous releases, which offered limited visibility into replication health and errors.&lt;/P&gt;
&lt;H2&gt;Unblocking Mirroring for servers with Read Replica enables&lt;/H2&gt;
&lt;P&gt;We also removed existing block for creating Mirrored databases on Flexible Servers with Read Replicas created. Now primary servers with one or more Read Replicas can serve as source for Mirroring their databases in Fabric with no limitations.&lt;/P&gt;
&lt;H2&gt;Conclusion&lt;/H2&gt;
&lt;P&gt;The new capabilities significantly reduce friction for analytics and AI workloads in Microsoft Fabric.&amp;nbsp;By removing technical barriers and increasing operational simplicity, the General Availability Refresh empowers organizations to unlock the full potential of their PostgreSQL data for advanced analytics and AI applications.&lt;/P&gt;
&lt;P&gt;The General Availability Refresh of Mirroring Azure Database for PostgreSQL in Microsoft Fabric delivers critical enhancements: native data type support, expanded Flexible Server HA compatibility, replication identity improvements, and better transparency. Together, these features make mirroring more robust, flexible, and trustworthy for analytics and AI workloads.&lt;/P&gt;
&lt;P&gt;We encourage technical professionals, DBAs, and developers to adopt these new capabilities and explore how they can transform data integration in Microsoft Fabric. For more information, visit the official documentation and resources:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/postgresql/integration/concepts-fabric-mirroring" target="_blank" rel="noopener"&gt;Azure Database for PostgreSQL Documentation&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://learn.microsoft.com/fabric/mirroring/azure-database-postgresql" target="_blank" rel="noopener"&gt;Microsoft Fabric Documentation&lt;/A&gt;&lt;/LI&gt;
&lt;/UL&gt;</description>
      <pubDate>Wed, 22 Apr 2026 07:21:19 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/general-availability-refresh-mirroring-azure-database-for/ba-p/4511726</guid>
      <dc:creator>scoriani</dc:creator>
      <dc:date>2026-04-22T07:21:19Z</dc:date>
    </item>
    <item>
      <title>March 2026 Recap: Azure Database for PostgreSQL</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/march-2026-recap-azure-database-for-postgresql/ba-p/4511432</link>
      <description>&lt;P&gt;Hello Azure community,&lt;/P&gt;
&lt;P&gt;March was packed with major feature announcements for &lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/postgresql/" target="_blank" rel="noopener"&gt;Azure Database for PostgreSQL&lt;/A&gt;. From the general availability of SSDv2, cascading read replicas, to online migration and new monitoring capabilities for logical replication slots to help ensure slots are preserved, this update brings a range of improvements to performance, scale, and reliability.&amp;nbsp;&lt;/P&gt;
&lt;H1&gt;Features&lt;/H1&gt;
&lt;OL&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;A href="#community--1-ssdv2" target="_self" rel="noopener"&gt;SSDv2 - Generally Available&lt;/A&gt;&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;A href="#community--1-replica" target="_self" rel="noopener"&gt;Cascading Read replica - Generally Available&lt;/A&gt;&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;A href="#community--1-migrate" target="_self" rel="noopener"&gt;Online migration using PgOutput plugin - Generally Available&lt;/A&gt;&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;A href="#community--1-alloydb" target="_self" rel="noopener"&gt;Google AlloyDB as a migration source - Generally Available&lt;/A&gt;&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;A href="#community--1-edb" target="_self" rel="noopener"&gt;EDB Extended Server as a migration source - Generally Available&lt;/A&gt;&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;A href="#community--1-repslot" target="_self" rel="noopener"&gt;Logical replication slot synchronization metrics - Preview&lt;/A&gt;&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;A href="#community--1-defender" target="_self" rel="noopener"&gt;Defender Security Assessments - Preview&lt;/A&gt;&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;A href="#community--1-vscode" target="_self" rel="noopener"&gt;New enhancements in the PostgreSQL VS Code Extension&lt;/A&gt;&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;A href="#community--1-minorversion" target="_self" rel="noopener"&gt;Latest PostgreSQL minor versions: 18.3, 17.9, 16.13, 15.17, 14.22&lt;/A&gt;&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;A href="#community--1-extension" target="_self" rel="noopener"&gt;New extension support for PostgreSQL 18 on Azure Database for PostgreSQL&lt;/A&gt;&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;A href="#community--1-guide" target="_self" rel="noopener"&gt;Guide on PostgreSQL Buffer Cache Analysis, query rewriting and elastic clusters&lt;/A&gt;&lt;/STRONG&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;H2 id="ssdv2"&gt;SSDv2 - Generally Available&lt;/H2&gt;
&lt;P&gt;&lt;STRONG&gt;Premium SSD v2 is now generally available for Azure Database for PostgreSQL Flexible Server&lt;/STRONG&gt;, delivering significant performance and cost-efficiency improvements for I/O‑intensive workloads. It offers up to &lt;STRONG&gt;4× higher IOPS&lt;/STRONG&gt;, lower latency, and improved price‑performance.&lt;/P&gt;
&lt;P&gt;With &lt;STRONG&gt;independent scaling of storage and performance&lt;/STRONG&gt;, you only pay for what you need. Premium SSD v2 supports storage scaling up to &lt;STRONG&gt;64&lt;/STRONG&gt;&lt;STRONG&gt; TiB&lt;/STRONG&gt;, with performance reaching &lt;STRONG&gt;80,000 IOPS&lt;/STRONG&gt; and &lt;STRONG&gt;1,200&lt;/STRONG&gt;&lt;STRONG&gt; MiB/s throughput&lt;/STRONG&gt;, without tying performance to disk size. IOPS and throughput can be adjusted instantly, with no downtime.&lt;/P&gt;
&lt;P&gt;Additionally, built‑in baseline performance at no additional cost ensures consistent performance even for smaller deployments, making Premium SSD v2 a strong choice for modern, high‑demand PostgreSQL applications.&lt;/P&gt;
&lt;P&gt;For details about the Premium SSD v2 release, see the &lt;A href="https://techcommunity.microsoft.com/blog/adforpostgresql/premium-ssd-v2-is-now-generally-available-for-azure-database-for-postgresql/4508445?previewMessage=true" target="_blank" rel="noopener"&gt;GA Announcement Blog&lt;/A&gt; and &lt;A href="https://learn.microsoft.com/azure/postgresql/compute-storage/concepts-storage-premium-ssd-v2" target="_blank" rel="noopener"&gt;documentation&lt;/A&gt;&lt;/P&gt;
&lt;H2 id="replica"&gt;Cascading read replica - Generally available&lt;/H2&gt;
&lt;P&gt;Cascading read replicas are now generally available, giving customers greater flexibility to create read replicas from &lt;STRONG&gt;existing read replicas&lt;/STRONG&gt;. This capability supports up to &lt;STRONG&gt;two levels of replication&lt;/STRONG&gt; and up to &lt;STRONG&gt;30 read replicas&lt;/STRONG&gt; in total, with each read replica able to host up to &lt;STRONG&gt;five cascading replicas&lt;/STRONG&gt;.&lt;/P&gt;
&lt;P&gt;With cascading read replicas, you can more effectively distribute read traffic across multiple replicas, deploy regional or hierarchical read replicas closer to end users, reduce read latency, and improve overall query performance for read‑heavy workloads. In addition, we’ve rolled out switchover support for both intermediate and cascading read replicas, making it easier to manage replica topologies. Learn more about cascading read replicas through&amp;nbsp;&lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/postgresql/read-replica/concepts-read-replicas#create-cascading-read-replicas-preview" target="_blank" rel="noopener"&gt;our documentation&lt;/A&gt; and a &lt;A class="lia-internal-link lia-internal-url lia-internal-url-content-type-blog" href="https://techcommunity.microsoft.com/blog/adforpostgresql/cascading-read-replicas-now-generally-available/4510610" target="_blank" rel="noopener" data-lia-auto-title="detailed blog walkthrough." data-lia-auto-title-active="0"&gt;detailed blog walkthrough.&lt;/A&gt;&lt;/P&gt;
&lt;H2 id="migrate"&gt;Online migration using PgOutput plugin - Generally Available&lt;/H2&gt;
&lt;P&gt;The new addition of the &lt;STRONG&gt;PgOutput plugin&lt;/STRONG&gt; helps make your Online migration to Azure more &lt;STRONG&gt;robust and seamless&lt;/STRONG&gt;. The native "Out-of-the-Box" support that PgOutout offers is more suited for Online Production migrations compared to other logical decoding plugins. PgOutput offers &lt;STRONG&gt;higher throughput&lt;/STRONG&gt; and &lt;STRONG&gt;superior performance&lt;/STRONG&gt; compared to other logical decoding plugins ensuring your Online migration has very limited downtime. PgOutput also offers fine-grained filtering using Publications where you can migrate specific tables and filter by specific operations.&lt;/P&gt;
&lt;P&gt;For more details about this update, see the &lt;A href="https://learn.microsoft.com/azure/postgresql/migrate/migration-service/concepts-required-user-permissions#online-migration-using-pgoutput---required-publication-permissions" target="_blank" rel="noopener"&gt;documentation&lt;/A&gt;.&lt;/P&gt;
&lt;H2 id="alloydb"&gt;Google AlloyDB as a migration source - Generally Available&lt;/H2&gt;
&lt;P&gt;&lt;STRONG&gt;Google AlloyDB&lt;/STRONG&gt; is now supported as a source in Azure Database for PostgreSQL Migration Service. You can use this capability to migrate your AlloyDB workloads directly to Azure Database for PostgreSQL, using either&lt;STRONG&gt; offline&lt;/STRONG&gt; or &lt;STRONG&gt;online &lt;/STRONG&gt;migration options. This support helps you move your PostgreSQL databases to Azure with confidence, while taking advantage of Azure’s flexibility and scalability.&lt;/P&gt;
&lt;P&gt;To know more about this feature, visit our &lt;A href="https://learn.microsoft.com/azure/postgresql/migrate/migration-service/tutorial-migration-service-alloy-db-offline?tabs=portal" target="_blank" rel="noopener"&gt;documentation&lt;/A&gt;.&lt;/P&gt;
&lt;H2 id="edb"&gt;EDB Extended Server as a migration source - Generally Available&lt;/H2&gt;
&lt;P&gt;Azure Database for PostgreSQL Migration Service now supports &lt;STRONG&gt;EDB&lt;/STRONG&gt; Extended Server as a migration source. This enables you to migrate EDB Extended Server workloads to Azure Database for PostgreSQL using both offline and online migration methods. With this addition, you can transition PostgreSQL databases to Azure smoothly and benefit from the scale and flexibility of the Azure platform.&lt;/P&gt;
&lt;P&gt;For more details about this update, see the &lt;A href="https://learn.microsoft.com/azure/postgresql/migrate/migration-service/tutorial-migration-service-enterprise-db-extended-server-offline?tabs=portal" target="_blank" rel="noopener"&gt;documentation&lt;/A&gt;.&lt;/P&gt;
&lt;H2 id="repslot"&gt;Logical replication slot sync status metric - Preview&lt;/H2&gt;
&lt;P&gt;You can now monitor whether your logical replication slots are &lt;STRONG&gt;failover‑ready &lt;/STRONG&gt;using the new l&lt;STRONG&gt;ogical_replication_slot_sync_status&lt;/STRONG&gt; metric, now in preview. This metric provides a simple binary signal indicating whether logical replication slots are &lt;STRONG&gt;synchronized&lt;/STRONG&gt; across High availability (HA) &lt;STRONG&gt;primary and standby nodes&lt;/STRONG&gt;. It helps you quickly assess failover readiness without digging into replication internals especially valuable for CDC pipelines such as Debezium and Kafka, where data continuity during failover is critical.&lt;/P&gt;
&lt;P&gt;Learn more about &lt;A class="lia-external-url" href="https://aka.ms/pg-flex-replication-metrics" target="_blank" rel="noopener"&gt;logical replication metrics in the documentation&lt;/A&gt;.&lt;/P&gt;
&lt;H2 id="defender"&gt;Defender Security Assessments - Preview&lt;/H2&gt;
&lt;P&gt;In March, we introduced two new Microsoft Defender for Cloud CSPM security recommendations for Azure Database for PostgreSQL Flexible Server, now available in &lt;A href="https://learn.microsoft.com/en-us/azure/defender-for-cloud/release-notes-recommendations-alerts#recommendations-alerts-and-incidents-updates" target="_blank" rel="noopener"&gt;public preview&lt;/A&gt;:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Geo-redundant backups should be enabled&lt;/STRONG&gt; for PostgreSQL Servers&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;require_secure_transport should be set to "on"&lt;/STRONG&gt; for PostgreSQL Servers&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;These integrated assessments continuously evaluate database configuration settings against security best practices, helping customers proactively identify and manage security posture risks for their Azure PostgreSQL servers while maintaining alignment with internal and industry standards.&lt;/P&gt;
&lt;P&gt;Additional security posture assessments for Azure PostgreSQL will be introduced as they become available.&amp;nbsp;&lt;/P&gt;
&lt;P&gt;To learn more, refer to the&amp;nbsp;&lt;A href="https://learn.microsoft.com/en-us/azure/defender-for-cloud/recommendations-reference-data#geo-redundant-backups-should-be-enabled-for-postgresql-servers" target="_blank" rel="noopener"&gt;reference table for all data security recommendations in Microsoft Defender for Cloud.&lt;/A&gt;&lt;/P&gt;
&lt;H2 id="vscode"&gt;New enhancements in the PostgreSQL VS Code Extension&lt;/H2&gt;
&lt;P&gt;&lt;A class="lia-external-url" href="https://github.com/microsoft/vscode-pgsql/blob/main/CHANGELOG.md" target="_blank" rel="noopener"&gt;The March release (v1.20)&lt;/A&gt; of the &lt;A class="lia-external-url" href="https://marketplace.visualstudio.com/items?itemName=ms-ossdata.vscode-pgsql" target="_blank" rel="noopener"&gt;PostgreSQL VS Code extension&lt;/A&gt; delivers new server management capabilities, enhanced query plan analysis, visual improvements, and a batch of bug fixes.&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Clone Server: &lt;/STRONG&gt;You can now clone an Azure PostgreSQL Flexible Server directly from within the extension. The clone operation is available from the server management UI, allowing you to duplicate a server configuration including region, SKU, and settings without leaving VS Code.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Entra ID Authentication for AI-Powered Schema Conversion: &lt;/STRONG&gt;The Oracle-to-PostgreSQL migration experience now supports Microsoft Entra ID authentication for Azure OpenAI connectivity, replacing API key–based authentication. This enables enterprise-grade identity management and access control for AI-powered schema conversion workflows.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Query Plan Visualization Improvements: &lt;/STRONG&gt;The Copilot-powered “Analyze with Copilot” feature for query plans has been improved with more relevant optimization recommendations and smoother SQL attachment handling during plan analysis.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Apache AGE Graph Visualizer Enhancements: &lt;/STRONG&gt;The graph visualizer received a visual refresh with modernized edge rendering, a color-coded legend, and a new properties pane for exploring element details.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Object Explorer Deep Refresh: &lt;/STRONG&gt;The Object Explorer now supports refreshing expanded nodes in place, so newly created tables and objects appear immediately without needing to disconnect and reconnect.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Settings Management: &lt;/STRONG&gt;The extension now supports both global user settings and local .vscode/settings.json, providing more robust connection settings management across configuration sources.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Bug Fixes: &lt;/STRONG&gt;This release includes numerous bug fixes across script generation (DDL for triggers, materialized views, and functions), IntelliSense (foreign table support), JSON data export, query execution, and server connectivity.&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2 id="minorversion"&gt;Latest PostgreSQL minor versions: 18.3, 17.9, 16.13, 15.17, 14.22&lt;/H2&gt;
&lt;P&gt;Azure PostgreSQL now supports the latest PostgreSQL minor versions:&amp;nbsp;&lt;STRONG&gt;18.3, 17.9, 16.13, 15.17, and 14.22&lt;/STRONG&gt;. These updates are applied automatically during planned maintenance windows, ensuring your databases stay up to date with critical fixes and reliability improvements, with no manual action required. This is an out-of-cycle release that addresses regressions identified in the previous update. The release includes fixes across replication, JSON functions, query correctness, indexing, and extensions like &lt;EM&gt;pg_trgm&lt;/EM&gt;, improving overall stability and correctness of database operations.&lt;/P&gt;
&lt;P&gt;&lt;BR /&gt;For details about the minor release, see the &lt;A href="https://www.postgresql.org/about/news/postgresql-183-179-1613-1517-and-1422-released-3246/" target="_blank" rel="noopener"&gt;PostgreSQL announcement&lt;/A&gt;.&lt;/P&gt;
&lt;H2 id="extension"&gt;New extension support for PostgreSQL 18 on Azure Database for PostgreSQL&lt;/H2&gt;
&lt;P&gt;Azure Database for PostgreSQL running PostgreSQL 18 now supports extensions that enable&lt;STRONG&gt; graph querying,&lt;/STRONG&gt; &lt;STRONG&gt;in&lt;/STRONG&gt;‑&lt;STRONG&gt;database AI integration&lt;/STRONG&gt;, &lt;STRONG&gt;external storage access&lt;/STRONG&gt;, and &lt;STRONG&gt;scalable vector similarity search&lt;/STRONG&gt;, expanding the types of workloads that can be handled directly within PostgreSQL.&lt;/P&gt;
&lt;P&gt;Newly supported extensions include:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;AGE (Apache AGE v1.7.0): &lt;/STRONG&gt;Adds native graph data modeling and querying capabilities to PostgreSQL using openCypher, enabling hybrid relational–graph workloads within the same database.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;azure_ai: &lt;/STRONG&gt;Enables direct invocation of Microsoft Foundry models from PostgreSQL using SQL, allowing AI inference and embedding generation to be integrated into database workflows.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;azure_storage: &lt;/STRONG&gt;Provides native integration with Azure Blob Storage, enabling PostgreSQL to read from and write to external storage for data ingestion, export, and hybrid data architectures.&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/postgresql/extensions/how-to-use-pgdiskann" target="_blank" rel="noopener"&gt;pg_diskann&lt;/A&gt;&lt;STRONG&gt;: &lt;/STRONG&gt;Introduces disk‑based approximate nearest neighbor (ANN) indexing for high-performance vector similarity search at scale, optimized for large vector datasets with constrained memory.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Together, these extensions allow PostgreSQL on Azure to support multi-model, AI‑assisted, and data‑intensive workloads while preserving compatibility with the open‑source PostgreSQL ecosystem.&lt;/P&gt;
&lt;H2 id="guide"&gt;Guide on PostgreSQL buffer cache analysis, query rewriting&lt;/H2&gt;
&lt;P&gt;We have rolled out two new blogs on PostgreSQL buffer cache analysis and PostgreSQL query rewriting and subqueries. These blogs help you better understand how PostgreSQL behaves under the hood and how to apply practical performance optimizations whether you’re diagnosing memory usage, reducing unnecessary disk I/O, or reshaping queries to get more efficient execution plans as your workloads scale.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;PostgreSQL Buffer Cache Analysis&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;This blog focuses on &lt;A href="https://techcommunity.microsoft.com/blog/adforpostgresql/postgresql-buffer-cache-analysis/4501264" target="_blank" rel="noopener" data-lia-auto-title="understanding PostgreSQL memory behavior through shared_buffers" data-lia-auto-title-active="0"&gt;understanding PostgreSQL memory behavior through shared_buffers&lt;/A&gt;, the database’s primary buffer cache. Using native statistics and the pg_buffercache extension, it provides a data‑driven approach to evaluate cache efficiency, identify when critical tables and indexes are served from memory, and detect cases where disk I/O may be limiting performance. The guide offers a repeatable methodology to support informed tuning decisions as workloads scale.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;PostgreSQL Query Rewriting and Subqueries&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;This blog explores &lt;A class="lia-internal-link lia-internal-url lia-internal-url-content-type-blog" href="https://techcommunity.microsoft.com/blog/adforpostgresql/postgresql-query-rewriting-and-subqueries/4499819" target="_blank" rel="noopener" data-lia-auto-title="how query structure directly impacts PostgreSQL execution plans and performance" data-lia-auto-title-active="0"&gt;how query structure directly impacts PostgreSQL execution plans and performance&lt;/A&gt;. It walks through common anti‑patterns and practical rewrites such as replacing correlated subqueries with set‑based joins, using semi‑joins, and pre‑aggregating large tables to reduce unnecessary work and enable more efficient execution paths. Each scenario includes clear explanations, example rewrites, and self‑contained test scripts you can run.&lt;/P&gt;
&lt;H1&gt;Azure Postgres Learning Bytes 🎓&lt;/H1&gt;
&lt;H4&gt;How to create and store vector embeddings in Azure Database for PostgreSQL&lt;/H4&gt;
&lt;P&gt;Vector embeddings sit at the core of many modern AI applications from semantic search and recommendations to RAG‑based experiences. But once you generate embeddings, an important question follows: how do you generate and store them in your existing database server?&lt;/P&gt;
&lt;P&gt;With Azure Database for PostgreSQL, you can generate and store vector embeddings directly alongside your application data. By using the `&lt;EM&gt;azure_ai`&lt;/EM&gt; extension, PostgreSQL can seamlessly integrate with Azure OpenAI to create embeddings and store them in your database. This learning byte walks you through a step‑by‑step guide to generating and storing vector embeddings in Azure Database for PostgreSQL.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Step 1: Enable the Azure AI extension&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Azure Database for PostgreSQL supports the &lt;STRONG&gt;azure_ai &lt;/STRONG&gt;extension, which allows you to call &lt;STRONG&gt;Azure OpenAI&lt;/STRONG&gt; service.&lt;/P&gt;
&lt;P&gt;Connect to your database and run:&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;CREATE EXTENSION IF NOT EXISTS azure_ai;&lt;/LI-CODE&gt;
&lt;P&gt;&lt;STRONG&gt;Step 2: Create (or use existing) Azure OpenAI resource&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;You need an &lt;STRONG&gt;Azure OpenAI&lt;/STRONG&gt; resource in your subscription with an embedding model deployed.&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;In the Azure portal, create an &lt;STRONG&gt;Azure OpenAI&lt;/STRONG&gt; resource.&lt;/LI&gt;
&lt;LI&gt;Deploy an embedding model (for example, text-embedding-3-small).&lt;/LI&gt;
&lt;/OL&gt;
&lt;P&gt;Azure OpenAI provides the &lt;STRONG&gt;endpoint URL&lt;/STRONG&gt; and &lt;STRONG&gt;API key&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Step 3: Get endpoint and API key&lt;/STRONG&gt;&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;Go to your &lt;STRONG&gt;Azure OpenAI resource&lt;/STRONG&gt; in the Azure portal.&lt;/LI&gt;
&lt;LI&gt;Select &lt;STRONG&gt;Keys and Endpoint&lt;/STRONG&gt;.&lt;/LI&gt;
&lt;LI&gt;Copy:
&lt;UL&gt;
&lt;LI&gt;
&lt;P&gt;Endpoint&lt;/P&gt;
&lt;/LI&gt;
&lt;LI&gt;
&lt;P&gt;API Key (Key 1 or Key 2)&lt;/P&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;/LI&gt;
&lt;/OL&gt;
&lt;P&gt;&lt;STRONG&gt;Step 4: Configure Azure AI extension with OpenAI details&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Store the endpoint and key securely inside PostgreSQL&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;SELECT 
  azure_ai.set_setting(
    'azure_openai.endpoint', 'https://&amp;lt;your-endpoint&amp;gt;.openai.azure.com'
  );
SELECT 
  azure_ai.set_setting(
    'azure_openai.subscription_key', 
    '&amp;lt;your-api-key&amp;gt;'
  );
&lt;/LI-CODE&gt;
&lt;P&gt;&lt;STRONG&gt;Step 5: Generate an embedding&lt;/STRONG&gt;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;SELECT 
  LEFT(
    azure_openai.create_embeddings(
      'text-embedding-3-small', 'Sample text for PostgreSQL Lab'
    ):: text, 
    100
  ) AS vector_preview;
&lt;/LI-CODE&gt;
&lt;P&gt;&lt;STRONG&gt;Step 6: Add a vector column&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Add a vector column to store embeddings (example uses 1536‑dimensional vectors):&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;ALTER TABLE 
  &amp;lt; table - name &amp;gt; 
ADD 
  COLUMN embedding VECTOR(1536);&lt;/LI-CODE&gt;
&lt;P&gt;&lt;STRONG&gt;Step 7: Store the embedding&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Update your table with the generated embedding:&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;UPDATE 
  &amp;lt; table - name &amp;gt; 
SET 
  embedding = azure_openai.create_embeddings(
    'text-embedding-3-small', content
  );
&lt;/LI-CODE&gt;
&lt;H1&gt;Conclusion&lt;/H1&gt;
&lt;P&gt;That’s a wrap for our March 2026 recap. This month brought a set of meaningful updates focused on making Azure Database for PostgreSQL more performant, reliable, and scalable whether you’re modernizing workloads, scaling globally, or strengthening your security posture.&lt;/P&gt;
&lt;P&gt;We’ll be back soon with more exciting announcements and key feature enhancements for Azure Database for PostgreSQL, so stay tuned! Your feedback is important to us, have suggestions, ideas, or questions? We’d love to hear from you:&amp;nbsp;&lt;A href="https://aka.ms/pgfeedback" target="_blank" rel="noopener"&gt;https://aka.ms/pgfeedback&lt;/A&gt;.&lt;/P&gt;
&lt;P&gt;Follow us here for the latest announcements, feature releases, and best practices:&amp;nbsp;&lt;A href="https://techcommunity.microsoft.com/category/azuredatabases/blog/adforpostgresql" target="_blank" rel="noopener" data-lia-auto-title="Microsoft Blog for PostgreSQL" data-lia-auto-title-active="0"&gt;Microsoft Blog for PostgreSQL&lt;/A&gt;.&lt;/P&gt;</description>
      <pubDate>Wed, 15 Apr 2026 18:58:45 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/march-2026-recap-azure-database-for-postgresql/ba-p/4511432</guid>
      <dc:creator>gauri-kasar</dc:creator>
      <dc:date>2026-04-15T18:58:45Z</dc:date>
    </item>
    <item>
      <title>Combining pgvector and Apache AGE - knowledge graph &amp; semantic intelligence in a single engine</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/combining-pgvector-and-apache-age-knowledge-graph-semantic/ba-p/4508781</link>
      <description>&lt;P&gt;&lt;EM&gt;Inspired by &lt;A class="lia-internal-link lia-internal-url lia-internal-url-content-type-blog" href="https://techcommunity.microsoft.com/blog/adforpostgresql/graphrag-and-postgresql-integration-in-docker-with-cypher-query-and-ai-agents-ve/4503586" target="_blank" rel="noopener" data-lia-auto-title="GraphRAG and PostgreSQL Integration in Docker with Cypher Query and AI Agents" data-lia-auto-title-active="0"&gt;GraphRAG and PostgreSQL Integration in Docker with Cypher Query and AI Agents&lt;/A&gt;, which demonstrated how Apache AGE brings Cypher based graph querying into PostgreSQL for GraphRAG pipelines. This post takes that idea further combining AGE's graph traversal with pgvector's semantic search to build a unified analytical engine where vectors and graphs reinforce each other in a single PostgreSQL instance.&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;&lt;EM&gt;This post targets workloads where entity types, relationship semantics, and schema cardinality are known before ingestion. Embeddings are generated from structured attribute fields; graph edges are typed and written by deterministic ETL. No LLM is involved at any stage. You should use this approach when you have structured data and need operational query performance, and deterministic, auditable, sub-millisecond retrieval.&lt;/EM&gt;&lt;/P&gt;
&lt;H2&gt;&lt;STRONG&gt;The problem nobody talks about the multi database/ multi hop tax&lt;/STRONG&gt;&lt;/H2&gt;
&lt;P&gt;If you run technology for a large enterprise, you already know the data problem. It is not that you do not have enough data. It is that your data lives in too many places, connected by too many fragile pipelines, serving too many conflicting views of the same reality.&lt;/P&gt;
&lt;P&gt;Here is a pattern that repeats across industries. One team needs to find entities "similar to" a reference item — not by exact attribute match, but by semantic meaning derived from unstructured text like descriptions, reviews, or specifications. That is a vector similarity problem.&lt;/P&gt;
&lt;P&gt;Another team needs to traverse relationships trace dependency chains, map exposure paths, or answer questions like "if this node is removed, what downstream nodes are affected?" That is a graph traversal problem.&lt;/P&gt;
&lt;P&gt;Meanwhile, the authoritative master data of IDs, attributes, pricing, transactional history already lives in Postgres.&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;Now you are operating three databases. Three bills. Three sets of credentials. Three backup strategies. A fragile ETL layer stitching entity IDs across systems, breaking silently whenever someone adds a new attribute to the master table. And worst of all, nobody can ask a question that spans all three systems without custom application code.&lt;/P&gt;
&lt;P&gt;Azure PostgreSQL database can already do all three jobs. Two extensions&amp;nbsp;&lt;STRONG&gt;pgvector&lt;/STRONG&gt;&amp;nbsp;for vector similarity search and&amp;nbsp;&lt;STRONG&gt;Apache AGE&lt;/STRONG&gt;&amp;nbsp;extension for graph traversal bringing these capabilities natively into the database. No new infrastructure. No sync pipelines. No multi database tax!&lt;/P&gt;
&lt;P&gt;This post walks through exactly how to combine them, why each piece matters at scale, and what kinds of queries become possible when you stop treating vectors and graphs as separate concerns.&lt;/P&gt;
&lt;H2&gt;&lt;STRONG&gt;The architecture: Two extensions, One engine&lt;/STRONG&gt;&lt;/H2&gt;
&lt;P&gt;pgvector&amp;nbsp;adds a native&amp;nbsp;vector&amp;nbsp;data type and distance operators (&amp;lt;=&amp;gt;,&amp;nbsp;&amp;lt;-&amp;gt;,&amp;nbsp;&amp;lt;#&amp;gt;) with HNSW and IVFFlat index support.&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;A class="lia-external-url" href="https://aka.ms/pg-diskann-blog" target="_blank" rel="noopener"&gt;pg_diskann&lt;/A&gt;&amp;nbsp;adds a third index type that keeps the index on disk instead of in memory, enabling large scale vector search without proportional RAM.&lt;/P&gt;
&lt;P&gt;&lt;U&gt;&lt;STRONG&gt;&lt;EM&gt;example 1&lt;/EM&gt;&lt;/STRONG&gt;&lt;/U&gt;&lt;EM&gt;&lt;U&gt; &lt;/U&gt;- to run a product similarity query such as the one below which corelates products sold across multiple markets which are related (cosine similarity).&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;-&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; The limit clause in sub query limits the similarity search to closest 1 product recommendation&lt;/P&gt;
&lt;P&gt;-&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; High similarity score of &amp;gt; 0.75 (aka 75% similarity in embeddings)&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;-- Table DDL - for illuatration purposes only 
CREATE TABLE IF NOT EXISTS products (
    id              SERIAL PRIMARY KEY,
    sku             TEXT UNIQUE NOT NULL,
    name            TEXT NOT NULL,
    brand           TEXT NOT NULL,
    category        TEXT NOT NULL,
    subcategory     TEXT,
    market          TEXT NOT NULL,
    region          TEXT,
    description     TEXT,
    ingredients     TEXT,
    avg_rating      FLOAT DEFAULT 0.0,
    review_count    INT DEFAULT 0,
    price_usd       FLOAT,
    launch_year     INT,
    status          TEXT DEFAULT 'active',         
    embedding       vector(384)               
);&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;SELECT     us.name                                                    AS us_product,
           us.brand                                                   AS us_brand,
           in_p.name                                                  AS india_match,
           in_p.brand                                                 AS india_brand,
           Round((1 - (us.embedding &amp;lt;=&amp;gt; in_p.embedding))::NUMERIC, 4) AS similarity
FROM       products us
cross join lateral
           (
                    SELECT   name,
                             brand,
                             embedding
                    FROM     products
                    WHERE    market = 'India'
                    AND      category = us.category
                    ORDER BY embedding &amp;lt;=&amp;gt; us.embedding limit 1 ) in_p
WHERE      us.market = 'US'
AND        us.category = 'Skincare'
AND        us.avg_rating &amp;gt;= 4.0
AND        round((1 - (us.embedding &amp;lt;=&amp;gt; in_p.embedding))::NUMERIC, 4)&amp;gt; 0.75
ORDER BY   similarity DESC limit 20;&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;AGE adds a&amp;nbsp;cypher()&amp;nbsp;function that executes cypher queries against a labeled property graph stored in the database managed and maintained under the&amp;nbsp;ag_catalog&amp;nbsp;schema. Vertices and edges become first class PostgreSQL rows with&amp;nbsp;agtype properties.&lt;/P&gt;
&lt;P&gt;The age extension supports&amp;nbsp;MATCH,&amp;nbsp;CREATE,&amp;nbsp;MERGE,&amp;nbsp;WITH, and aggregations.&lt;/P&gt;
&lt;P&gt;&lt;U&gt;&lt;STRONG&gt;&lt;EM&gt;example 2&lt;/EM&gt;&lt;/STRONG&gt;&lt;/U&gt;&lt;EM&gt; - to run a product similarity query such as the one below which returns common products sold via multiple retail channels.&lt;/EM&gt;&lt;/P&gt;
&lt;LI-CODE lang="cypher"&gt;SET search_path = ag_catalog, "$user", public;

SELECT * FROM cypher('cpg_graph', $$
    MATCH (p:Product)-[:SOLD_AT]-&amp;gt;(walmart:RetailChannel {name: 'Walmart'})
    MATCH (p)-[:SOLD_AT]-&amp;gt;(target:RetailChannel {name: 'Target'})
    MATCH (b:Brand)-[:MANUFACTURES]-&amp;gt;(p)
    RETURN b.name     AS brand,
           p.name     AS product,
           p.category AS category,
           p.market   AS market,
           p.price_usd AS price
    ORDER BY p.category, b.name
$$) AS (brand agtype, product agtype, category agtype,
        market agtype, price agtype);
&lt;/LI-CODE&gt;
&lt;P&gt;The critical point and takeaway here is that both extensions participate in the same query planner and executor. A CTE that calls pgvector's &amp;lt;=&amp;gt; operator can feed results into a cypher() call in the next CTE all within a single transaction, sharing all available processes and control the database has to offer.&lt;/P&gt;
&lt;P&gt;Finally, you are looking at code that looks like -&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS age;

SET search_path = ag_catalog, "$user", public;
SELECT create_graph('knowledge_graph');
&lt;/LI-CODE&gt;
&lt;H2&gt;&lt;STRONG&gt;The bridge: pgvector → Apache AGE&lt;/STRONG&gt;&lt;/H2&gt;
&lt;P&gt;This is the architectural centrepiece where the mechanism that turns vector similarity scores into traversable graph edges. Without this “bridge” pgvector and AGE are two isolated extensions.&lt;/P&gt;
&lt;H4&gt;&lt;STRONG&gt;Why bridge at all?&lt;/STRONG&gt;&lt;/H4&gt;
&lt;LI-CODE lang="sql"&gt;pgvector answers: "What is similar to X?" 
AGE answers: "What is connected to Y, and how?" 
&lt;/LI-CODE&gt;
&lt;P&gt;These are fundamentally different questions operating on fundamentally different data structures. pgvector works on a flat vector space and every query is a distance calculation against an ANN index.&lt;/P&gt;
&lt;P&gt;AGE works on a labelled property graph where every query is a pattern match across typed nodes and edges.&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;What if now the question is – What is like X and connected to Y and how?&lt;/LI-CODE&gt;
&lt;P&gt;This is where the bridge gets activated comes into life.&lt;/P&gt;
&lt;P&gt;This takes cosine similarity distance scores from pgvector and writes them as&amp;nbsp;&lt;EM&gt;SIMILAR_TO&lt;/EM&gt;&amp;nbsp;edges in the AGE property graph turning a distance computation into a traversable relationship.&lt;/P&gt;
&lt;P&gt;Once similarity is an edge, cypher queries can then combine it with structural edges&amp;nbsp; in a single declarative pattern.&lt;/P&gt;
&lt;LI-CODE lang="cypher"&gt;for ind_prod_id, us_prod_id, similarity in pairs:
    execute_cypher(cur, f"""
        MATCH (a:Product {{product_id: { ind_prod_id }}}),
              (b:Product {{product_id: { us_prod_id }}})
        CREATE (a)-[:SIMILAR_TO {{score: {score:.4f},
                                  method: 'pgvector_cosine'}}]-&amp;gt;(b)
        CREATE (b)-[:SIMILAR_TO {{score: {score:.4f},
                                  method: 'pgvector_cosine'}}]-&amp;gt;(a)
    """)
&lt;/LI-CODE&gt;
&lt;P&gt;The cypher()&amp;nbsp;function translates Cypher into DML against&amp;nbsp;ag_catalog&amp;nbsp;tables under the hood, these are plain PostgreSQL heap inserts just like another row.&lt;/P&gt;
&lt;P&gt;The score property is the edge weight on the SIMILAR_TO relationship. Its value is the similarity score computed from pgvector using cosine similarity, so a higher score means the two products are more semantically similar.&lt;/P&gt;
&lt;P&gt;The method property is metadata on that same edge. It records how the score was produced. In this case, pgvector_cosine is just a string label indicating that the relationship was derived using pgvector based cosine similarity.&lt;/P&gt;
&lt;P&gt;Cosine similarity is symmetric, but property graph traversal is directional i.e. MATCH (a)-[:SIMILAR_TO]-&amp;gt;(b) won't find the reverse path unless both directional edges exist.&lt;/P&gt;
&lt;img /&gt;
&lt;H4&gt;&lt;STRONG&gt;Why this combination matters&lt;/STRONG&gt;&lt;/H4&gt;
&lt;P&gt;One backup strategy. One monitoring stack. One connection pool. One failover target. One set of credentials. One database restore considerations - for teams already running Az PostgreSQL databases in production this adds capabilities without adding any net new infrastructure.&lt;/P&gt;
&lt;H4&gt;&lt;STRONG&gt;Unified cost model&lt;/STRONG&gt;&lt;/H4&gt;
&lt;P&gt;The planner assigns cost estimates to index scan for both execution engines using the same cost framework it uses for B-tree lookups and sequential scans. It can decide whether to use the HNSW index or fall back to a sequential scan based on table statistics and server parameters.&lt;/P&gt;
&lt;P&gt;As you have learnt so far, there is no separate storage or database engine to learn.&lt;/P&gt;
&lt;H2&gt;&lt;STRONG&gt;Bringing all this knowledge together&lt;/STRONG&gt;&lt;/H2&gt;
&lt;P&gt;Examples 1 and 2 were all about native vector search and native graph search example in a classic product catalog scenario, respectively. Now, let’s bring this to life - &lt;EM&gt;What if now the question is – What is like X and connected to Y and how?&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;&lt;EM&gt;In this use case - pgvector finds the cross market matches (as shown in example 1), then Cypher checks which of those matches are sold at both Walmart and Target:&lt;/EM&gt;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;SET search_path = ag_catalog, "$user", public;

-- Cross-market matching (pgvector) → Retail channel overlap (graph)
WITH cross_market AS (
    SELECT us.id    AS us_id,
           us.name  AS us_product,
           us.brand AS us_brand,
           in_p.id    AS india_id,
           in_p.name  AS india_match,
           in_p.brand AS india_brand,
           ROUND((1 - (us.embedding &amp;lt;=&amp;gt; in_p.embedding))::numeric, 4) AS similarity
    FROM products us
    CROSS JOIN LATERAL (
        SELECT id, name, brand, embedding
        FROM products
        WHERE market = 'India'
          AND category = us.category
        ORDER BY embedding &amp;lt;=&amp;gt; us.embedding
        LIMIT 1
    ) in_p
    WHERE us.market = 'US'
      AND us.category = 'Skincare'
      AND us.avg_rating &amp;gt;= 4.0
      AND ROUND((1 - (us.embedding &amp;lt;=&amp;gt; in_p.embedding))::numeric, 4) &amp;gt; 0.75
),
dual_channel AS (
    SELECT (pid::text)::int AS product_id,
           brand::text       AS brand
    FROM cypher('cpg_graph', $$
        MATCH (p:Product)-[:SOLD_AT]-&amp;gt;(w:RetailChannel {name: 'Walmart'})
        MATCH (p)-[:SOLD_AT]-&amp;gt;(t:RetailChannel {name: 'Target'})
        MATCH (b:Brand)-[:MANUFACTURES]-&amp;gt;(p)
        RETURN p.product_id AS pid,
               b.name       AS brand
    $$) AS (pid agtype, brand agtype)
)
SELECT cm.us_product,
       cm.us_brand,
       cm.india_match,
       cm.india_brand,
       cm.similarity,
       CASE WHEN dc.product_id IS NOT NULL
            THEN 'Yes' ELSE 'No'
       END AS india_match_at_walmart_and_target
FROM cross_market cm
LEFT JOIN dual_channel dc ON dc.product_id = cm.india_id
ORDER BY cm.similarity DESC
LIMIT 20;
&lt;/LI-CODE&gt;
&lt;H2&gt;&lt;STRONG&gt;Conclusion&lt;/STRONG&gt;&lt;/H2&gt;
&lt;P&gt;The Azure PostgreSQL database ecosystem has quietly assembled the components for a unified semantic + structural analytics engine in form of extensions.&lt;/P&gt;
&lt;P&gt;pgvector with pg_diskann delivers production grade approximate nearest-neighbour search with ANN indexes.&lt;/P&gt;
&lt;P&gt;Apache AGE delivers cypher based property graph traversal. Together with a “bridge,” they enable query patterns that are impossible in either system alone and they do it within the ACID guarantees, operational tooling, and SQL vocabulary knowledge you already have.&lt;/P&gt;
&lt;P&gt;Stop paying for three databases when one will do!&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;</description>
      <pubDate>Wed, 15 Apr 2026 11:59:07 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/combining-pgvector-and-apache-age-knowledge-graph-semantic/ba-p/4508781</guid>
      <dc:creator>Raunak</dc:creator>
      <dc:date>2026-04-15T11:59:07Z</dc:date>
    </item>
    <item>
      <title>Cascading Read Replicas Now Generally Available!</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/cascading-read-replicas-now-generally-available/ba-p/4510610</link>
      <description>&lt;P&gt;We’re excited to announce the &lt;STRONG&gt;General Availability of &lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/postgresql/read-replica/concepts-read-replicas#create-cascading-read-replicas-preview" target="_blank"&gt;cascading read replicas in Azure Database for PostgreSQL&lt;/A&gt;&lt;/STRONG&gt;. This capability allows you to create read replicas for your Azure Database for PostgreSQL instance not only from a primary server, but also from existing read replicas, enabling &lt;STRONG&gt;multi‑level replication chains&lt;/STRONG&gt;.&lt;/P&gt;
&lt;P&gt;Coordinating read‑heavy database workloads across multiple regions can be challenging, especially when you’re trying to deliver low‑latency read response experiences to users spread across different geographic locations. One effective way to address this is by placing read replicas closer to where your users are, allowing applications to serve read requests with significantly reduced latency and improved performance.&lt;/P&gt;
&lt;H2&gt;What are cascading read replicas?&lt;/H2&gt;
&lt;P&gt;With cascading read replicas, you can scale read‑intensive workloads more effectively, distribute read traffic efficiently, and support advanced deployment topologies such as globally distributed applications. Each read replica can act as a source for additional replicas, forming a &lt;STRONG&gt;tree‑like replication structure&lt;/STRONG&gt;. For example, if your primary server is deployed in one region, you can create direct replicas in nearby regions and then cascade additional replicas to more distant locations. This approach helps spread read traffic evenly while minimizing latency for users around the world. We support up to 2 levels of replication with this feature. Level 1 will be all the read replicas and level 2 will be cascading read replicas.&lt;/P&gt;
&lt;H2&gt;Why use cascading read replicas?&lt;/H2&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Improved scalability&lt;/STRONG&gt;&lt;BR /&gt;Cascading read replicas support multi‑level replication, making it easier to handle high volumes of read traffic without overloading a single instance by scaling up to 30 read replicas.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Geographic distribution&lt;/STRONG&gt;&lt;BR /&gt;By placing replicas closer to your global user base, you can significantly reduce read latency and deliver faster, more responsive application experiences.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Efficient read traffic distribution&lt;/STRONG&gt;&lt;BR /&gt;Distributing read workloads across multiple replicas helps balance load, improving overall performance and reliability.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Additionally, cascading read replicas offer operational flexibility. If you observe replication lag, you can &lt;STRONG&gt;perform a switchover operation between a cascading read replica with its source or intermediate replica&lt;/STRONG&gt;, helping you maintain optimal performance and availability for your replicas.&lt;/P&gt;
&lt;H2&gt;How does replication work with cascading read replicas?&lt;/H2&gt;
&lt;P&gt;The primary server acts as a source for the read replica. Data is asynchronously replicated to these replicas. When we add cascading replicas, the previous replicas act as a data source for replication.&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;In the diagram above, “primary-production-server” is the primary server with three read replicas. One of these replicas, “readreplica01”, serves as the source for another read replica, “readreplica11” which is a cascading read replica.&lt;/P&gt;
&lt;P&gt;With cascading read replicas, you can add up to five read replicas per source and replicate data across two levels, as shown in the diagram. This allows you to create up to 30 read replicas in total five read replicas directly from the primary server, and up to 25 additional replicas at the second level (each second-level replica can have up to five read replicas).&lt;/P&gt;
&lt;P&gt;If you notice replication lag between an intermediate read replica and a cascading read replica, you can use a switchover operation to swap “readreplica01” and “readreplica11”, helping reduce the impact of lag.&lt;/P&gt;
&lt;P&gt;To learn more about cascading read replicas, please refer to our documentation: &lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/postgresql/read-replica/concepts-read-replicas#create-cascading-read-replicas" target="_blank"&gt;Cascading read replicas&lt;/A&gt;&lt;/P&gt;
&lt;H2&gt;Deploying cascading read replicas on Azure portal&lt;/H2&gt;
&lt;OL&gt;
&lt;LI&gt;Navigate to the “Replication” tab and then click on “Create replica” highlighted in red as shown below:&lt;img /&gt;&lt;/LI&gt;
&lt;LI&gt;After creating a read replica as the below screenshot shows that you have 1 read replica that is attached to the primary instance.&lt;img /&gt;&lt;/LI&gt;
&lt;LI&gt;Click on the created replica and navigate to the replication tab, source server is “read-replica-01” and we will be creating a cascading read replica under this.&lt;img /&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;/LI&gt;
&lt;LI&gt;Once cascading read replica is created you can see the role of “read-replica-01” has now changed to Source, Replica. You can perform site swap operation by clicking on the promote button for cascading read replica.&lt;img /&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;H2&gt;Deploy cascading read replica with terraform:&lt;/H2&gt;
&lt;P&gt;Before you start, make sure you have:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;An existing &lt;STRONG&gt;primary PostgreSQL Flexible Server&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI&gt;At least &lt;STRONG&gt;one read replica&lt;/STRONG&gt; already created from the primary&lt;/LI&gt;
&lt;LI&gt;AzureRM provider with latest version&lt;/LI&gt;
&lt;LI&gt;Proper permissions on the Azure subscription and resource group&lt;/LI&gt;
&lt;/UL&gt;
&lt;OL&gt;
&lt;LI&gt;Configure the AzureRM Provider: Start by configuring the AzureRM provider in your Terraform project.&lt;LI-CODE lang="shell-session"&gt;terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~&amp;gt; 3.80"
    }
  }
}

provider "azurerm" {
  features {}
}&lt;/LI-CODE&gt;&lt;/LI&gt;
&lt;LI&gt;
&lt;P&gt;Reference the existing read replica server using the data block to reference the replica server.&lt;/P&gt;
&lt;LI-CODE lang="shell-session"&gt;data "azurerm_postgresql_flexible_server" "source_replica" {
  name                = "my-read-replica-1"
  resource_group_name = "my-resource-group"
}&lt;/LI-CODE&gt;&lt;/LI&gt;
&lt;LI&gt;
&lt;P&gt;Now create a new PostgreSQL Flexible Server and point it to the replica using create_source_server_id.&lt;/P&gt;
&lt;LI-CODE lang="shell-session"&gt;resource "azurerm_postgresql_flexible_server" "cascading_replica" {
  name                   = "my-cascading-replica"
  resource_group_name    = "my-resource-group"
  location               = data.azurerm_postgresql_flexible_server.source_replica.location
  version                = data.azurerm_postgresql_flexible_server.source_replica.version

  delegated_subnet_id    = data.azurerm_postgresql_flexible_server.source_replica.delegated_subnet_id
  private_dns_zone_id    = data.azurerm_postgresql_flexible_server.source_replica.private_dns_zone_id

  create_mode            = "Replica"
  create_source_server_id = data.azurerm_postgresql_flexible_server.source_replica.id

  storage_mb             = 32768
  sku_name               = "Standard_D4s_v3"

  depends_on = [
    data.azurerm_postgresql_flexible_server.source_replica
  ]
}&lt;/LI-CODE&gt;&lt;/LI&gt;
&lt;LI&gt;
&lt;P&gt;Apply the Terraform Configuration&lt;/P&gt;
&lt;LI-CODE lang="shell-session"&gt;terraform init
terraform plan
terraform apply&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;/LI&gt;
&lt;/OL&gt;
&lt;H2&gt;Key Considerations&lt;/H2&gt;
&lt;OL&gt;
&lt;LI&gt;Cascading read replicas allow for up to 5 read replicas and two levels of replication.&lt;/LI&gt;
&lt;LI&gt;Creating cascading read replicas is supported in PostgreSQL version 14 and above.&lt;/LI&gt;
&lt;LI&gt;Promote operation is not supported for intermediate read replicas with cascading read replicas.&lt;/LI&gt;
&lt;/OL&gt;
&lt;H2&gt;Conclusion&lt;/H2&gt;
&lt;P&gt;Cascading read replicas in Azure Database for PostgreSQL offer a scalable way to distribute your read traffic across the same and different regions, reducing the read workload on primary database. For globally distributed applications, this can improve read latency as well as resilience and performance. This design supports horizontal scaling as your application demand grows, ensuring you can handle a high volume of read requests without compromising speed. Get started with this feature today and scale your read workloads.&lt;/P&gt;</description>
      <pubDate>Mon, 13 Apr 2026 17:23:08 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/cascading-read-replicas-now-generally-available/ba-p/4510610</guid>
      <dc:creator>gauri-kasar</dc:creator>
      <dc:date>2026-04-13T17:23:08Z</dc:date>
    </item>
    <item>
      <title>Premium SSD v2 Is Now Generally Available for Azure Database for PostgreSQL</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/premium-ssd-v2-is-now-generally-available-for-azure-database-for/ba-p/4508445</link>
      <description>&lt;P class="lia-align-left"&gt;We are excited to announce the General Availability (GA) of &lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/postgresql/compute-storage/concepts-storage-premium-ssd-v2" target="_blank" rel="noopener"&gt;Premium SSD v2&lt;/A&gt; for Azure Database for PostgreSQL flexible server. With Premium SSD v2, you can achieve&amp;nbsp;&lt;STRONG&gt;up to 4× higher IOPS, significantly lower latency, and better price-performance&lt;/STRONG&gt; for I/O-intensive PostgreSQL workloads. With independent scaling of storage and performance, you can now eliminate overprovisioning and unlock predictable, high-performance PostgreSQL at scale.&lt;/P&gt;
&lt;P class="lia-align-left"&gt;This release is especially impactful for &lt;STRONG&gt;OLTP, SaaS, and high‑concurrency applications&lt;/STRONG&gt; that require consistent performance and reliable scaling under load.&lt;/P&gt;
&lt;P class="lia-align-left"&gt;&lt;STRONG&gt;In this post, we will cover:&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL class="lia-align-left"&gt;
&lt;LI&gt;&lt;STRONG&gt;Why Premium SSD v2&lt;/STRONG&gt;: Core capabilities such as flexible disk sizing, higher performance, and independent scaling of capacity and I/O.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Premium SSD v2 vs. Premium SSD:&lt;/STRONG&gt; A side‑by‑side overview of what’s new and what’s improved.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Pricing&lt;/STRONG&gt;: Pricing estimates.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Performance&lt;/STRONG&gt;: Benchmarking results across two workload scenarios.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Migration options:&lt;/STRONG&gt; How to move from Premium SSD to Premium SSD v2 using restore and read‑replica approaches.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Availability and support&lt;/STRONG&gt;: Regional availability, supported features, current limitations, and how to get started.&lt;/LI&gt;
&lt;/UL&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H1&gt;&lt;SPAN class="lia-text-color-15"&gt;Why Premium SSD v2?&lt;/SPAN&gt;&lt;/H1&gt;
&lt;/DIV&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;UL class="lia-align-left"&gt;
&lt;LI&gt;&lt;SPAN class="lia-text-color-21"&gt;&lt;STRONG&gt;Flexible Disk Size&lt;/STRONG&gt; - &lt;/SPAN&gt;Storage can be provisioned from&lt;STRONG&gt; 32 GiB to 64 TiB&lt;/STRONG&gt; in &lt;STRONG&gt;1 GiB&lt;/STRONG&gt; increments, allowing you to pay only for required capacity without scaling disk size for performance.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;High Performance&lt;/STRONG&gt; -Achieve up to&lt;STRONG&gt; 80,000 IOPS &lt;/STRONG&gt;and &lt;STRONG&gt;1,200 MiB/s &lt;/STRONG&gt;throughput on a single disk, enabling high-throughput OLTP and mixed workloads.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Adapt instantly to workload changes:&amp;nbsp;&lt;/STRONG&gt; With Premium SSD v2, performance is no longer tied to disk size. Independently tune IOPS and throughput without downtime, ensuring your database keeps up with real-time demand.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Free baseline performance:&lt;/STRONG&gt; Premium SSD v2 includes built-in baseline performance at no additional cost. Disks up to&amp;nbsp;&lt;STRONG&gt;399&lt;/STRONG&gt;&lt;STRONG&gt; GiB&lt;/STRONG&gt; automatically include &lt;STRONG&gt;3,000 IOPS and 125&lt;/STRONG&gt;&lt;STRONG&gt; MiB/s&lt;/STRONG&gt;, while disks sized &lt;STRONG&gt;400&lt;/STRONG&gt;&lt;STRONG&gt;&amp;nbsp;GiB and larger&lt;/STRONG&gt; include &lt;STRONG&gt;up to 12,000 IOPS and 500&lt;/STRONG&gt;&lt;STRONG&gt;&amp;nbsp;MiB/s&lt;/STRONG&gt;.&lt;/LI&gt;
&lt;/UL&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H1&gt;&lt;SPAN class="lia-text-color-15"&gt;Premium SSD v2 vs. Premium SSD: What’s new?&lt;/SPAN&gt;&lt;/H1&gt;
&lt;/DIV&gt;
&lt;P class="lia-clear-both lia-align-left"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H1&gt;&lt;SPAN class="lia-text-color-15"&gt;Pricing&lt;/SPAN&gt;&lt;/H1&gt;
&lt;/DIV&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P class="lia-align-left"&gt;Pricing for Premium SSD v2 is similar to Premium SSD, but will vary depending on the storage, IOPS, and bandwidth configuration set for a Premium SSD v2 disk. Pricing information is available on the&amp;nbsp;&lt;A href="https://azure.microsoft.com/pricing/details/postgresql/flexible-server/?msockid=0627d05ab15a6c403105c639b0d06d2c" target="_blank" rel="noopener"&gt;pricing page&lt;/A&gt; or &lt;A href="https://azure.microsoft.com/pricing/calculator/?msockid=0627d05ab15a6c403105c639b0d06d2c" target="_blank" rel="noopener"&gt;pricing calculator&lt;/A&gt;.&lt;/P&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H1&gt;&lt;SPAN class="lia-text-color-15"&gt;Performance&lt;/SPAN&gt;&lt;/H1&gt;
&lt;/DIV&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P class="lia-align-left"&gt;Premium SSD v2 is designed for IO‑intensive workloads that require sub‑millisecond disk latencies, high IOPS, and high throughput at a lower cost. To demonstrate the performance impact, we ran &lt;STRONG&gt;pgbench&lt;/STRONG&gt; on Azure Database for PostgreSQL using the test profile below.&lt;/P&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H4&gt;&lt;SPAN class="lia-text-color-15"&gt;Test Setup&lt;/SPAN&gt;&lt;/H4&gt;
&lt;/DIV&gt;
&lt;P class="lia-align-left"&gt;To minimize external variability and ensure a fair comparison:&lt;/P&gt;
&lt;UL class="lia-align-left"&gt;
&lt;LI&gt;Client virtual machines and the database server were deployed in the same availability zone in the East US region.&lt;/LI&gt;
&lt;LI&gt;Compute, region, and availability zones were kept identical.&lt;/LI&gt;
&lt;LI&gt;The only variable changed was the storage tier.&lt;/LI&gt;
&lt;LI&gt;TPC-B benchmark using pgbench with a database size of 350 GiB.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="lia-align-left"&gt;&lt;EM&gt;&amp;nbsp;&lt;/EM&gt;&lt;/P&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H2&gt;&lt;SPAN class="lia-text-color-15"&gt;Test Scenario 1: Breaking the IOPS Ceiling with Premium SSD v2&lt;/SPAN&gt;&lt;/H2&gt;
&lt;/DIV&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P class="lia-align-left"&gt;Premium SSD v2 eliminates the traditional storage bottleneck by scaling linearly up to &lt;STRONG&gt;80,000 IOPS&lt;/STRONG&gt;, while Premium SSD plateaus early due to fixed performance limits. To demonstrate this, we configured each storage tier with its maximum supported IOPS and throughput while keeping all other variables constant. Premium SSD v2 achieves up to &lt;STRONG&gt;4x higher IOPS at nearly half the cost&lt;/STRONG&gt;, without requiring large disk sizes.&lt;/P&gt;
&lt;P class="lia-align-left"&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;P class="lia-align-left"&gt;&amp;nbsp;&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN lia-align-left"&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;P class="lia-align-left"&gt;&amp;nbsp; &amp;nbsp;&lt;STRONG&gt;&lt;EM&gt;Note:&lt;/EM&gt; &lt;/STRONG&gt;&lt;EM&gt;Premium SSD requires a 32 TiB disk to reach 20K IOPS, while SSD v2 achieves 80K IOPS even on a 160 GiB disk though we used 1 TiB disk in this test for a bigger scaling factor for pgbench test&lt;/EM&gt;&lt;STRONG&gt;&lt;EM&gt;.&lt;/EM&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;P class="lia-align-left"&gt;We ran pgbench across five workload profiles, ranging from 32 to 256 concurrent clients, with each test running for 20 minutes. The results go beyond incremental improvements and highlight a material shift in how applications scale with Premium SSD v2.&lt;/P&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H3&gt;&lt;SPAN class="lia-text-color-15"&gt;Throughput Scaling&lt;/SPAN&gt;&lt;/H3&gt;
&lt;/DIV&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P class="lia-align-left"&gt;&lt;EM&gt;As concurrency increases, Premium SSD quickly reaches its IOPS limits while Premium SSD v2 continues to scale.&lt;/EM&gt;&lt;/P&gt;
&lt;UL class="lia-align-left"&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;EM&gt;At 32 clients&lt;/EM&gt;&lt;/STRONG&gt;&lt;EM&gt;: Premium SSD v2 achieved &lt;STRONG&gt;10,562 TPS&lt;/STRONG&gt; vs &lt;STRONG&gt;4,123 TPS&lt;/STRONG&gt; on Premium SSD representing a &lt;STRONG&gt;156%&lt;/STRONG&gt; performance improvement.&lt;/EM&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;EM&gt;At 256 clients&lt;/EM&gt;&lt;/STRONG&gt;&lt;EM&gt;: At higher load, Premium SSD v2 achieved over &lt;STRONG&gt;43,000 TPS&amp;nbsp;&lt;/STRONG&gt;representing a &lt;STRONG&gt;279% improvement&lt;/STRONG&gt; compared to the &lt;STRONG&gt;11,465 TPS&lt;/STRONG&gt; observed on Premium SSD.&lt;/EM&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="lia-align-left"&gt;&lt;STRONG&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; &lt;/STRONG&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H3&gt;&lt;SPAN class="lia-text-color-15"&gt;Latency Stability&lt;/SPAN&gt;&lt;/H3&gt;
&lt;/DIV&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P class="lia-align-left"&gt;Throughput is an indication of how much work is done while latency reflects how quickly users experience it. Premium SSD v2 maintains consistently low latency even as workload increases.&lt;/P&gt;
&lt;UL class="lia-align-left"&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;EM&gt;Reduced Wait Times&lt;/EM&gt;&lt;/STRONG&gt;: &lt;STRONG&gt;61–74%&lt;/STRONG&gt; lower latency across all test phases.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;EM&gt;Consistency under Load&lt;/EM&gt;&lt;/STRONG&gt;: Premium SSD latency increased to &lt;STRONG&gt;22.3&lt;/STRONG&gt;&lt;STRONG&gt; ms&lt;/STRONG&gt;, while Premium SSD v2 maintained a &lt;STRONG&gt;latency of 5.8&lt;/STRONG&gt;&lt;STRONG&gt; ms,&lt;/STRONG&gt; remaining stable even under peak load.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="lia-align-left"&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;P class="lia-align-left"&gt;&amp;nbsp;&lt;/P&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H3&gt;&lt;SPAN class="lia-text-color-15"&gt;IOPS Behavior&lt;/SPAN&gt;&lt;/H3&gt;
&lt;/DIV&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P class="lia-align-left"&gt;The table below illustrates the IOPS behavior observed during benchmarking for both storage tiers.&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN lia-align-left"&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN lia-align-left"&gt;&lt;table border="1" style="border-width: 1px;"&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class="lia-align-center"&gt;
&lt;P&gt;&lt;STRONG&gt;Dimension&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td class="lia-align-center"&gt;
&lt;P&gt;&lt;STRONG&gt;Premium SSD&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td class="lia-align-center"&gt;
&lt;P&gt;&lt;STRONG&gt;Premium SSD v2&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="lia-align-center" rowspan="2"&gt;
&lt;P&gt;&lt;STRONG&gt;IOPS &lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td rowspan="2"&gt;
&lt;P&gt;Lower baseline performance, Hits limits early&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;STRONG&gt;~2× higher IOPS&lt;/STRONG&gt; at low concurrency&lt;STRONG&gt;,&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;
&lt;P&gt;&lt;STRONG&gt;Up to 4× higher IOPS at peak load&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="lia-align-center"&gt;
&lt;P&gt;&lt;STRONG&gt;IOPS Plateau&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;Throughput stalls at &lt;STRONG&gt;~20k IOPS&lt;/STRONG&gt; for 64 clients -256 clients&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;Scales from &lt;STRONG&gt;~29k IOPS (32 clients)&lt;/STRONG&gt; to &lt;STRONG&gt;~80k IOPS (256 clients)&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="lia-align-center"&gt;
&lt;P&gt;&lt;STRONG&gt;Additional Clients&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;Adding clients does not increase throughput&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;Additional clients continue to drive higher throughput&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="lia-align-center"&gt;
&lt;P&gt;&lt;STRONG&gt;Primary Bottleneck&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;STRONG&gt;Storage becomes the bottleneck&lt;/STRONG&gt; early&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;No single bottleneck observed&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="lia-align-center"&gt;
&lt;P&gt;&lt;STRONG&gt;Scaling Behavior&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;Stops scaling early&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;STRONG&gt;True linear scaling&lt;/STRONG&gt; with workload demand&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="lia-align-center"&gt;
&lt;P&gt;&lt;STRONG&gt;Resource Utilization&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;Disk saturation leaves CPU and memory underutilized&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;STRONG&gt;Balanced utilization&lt;/STRONG&gt; across IOPS, CPU, and memory&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td class="lia-align-center"&gt;
&lt;P&gt;&lt;STRONG&gt;Key Takeaway&lt;/STRONG&gt;&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;Storage limits performance before compute is fully used&lt;/P&gt;
&lt;/td&gt;&lt;td&gt;
&lt;P&gt;&lt;STRONG&gt;Unlocks higher throughput and lower latency&lt;/STRONG&gt; by fully utilizing compute resources&lt;/P&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 33.33%" /&gt;&lt;col style="width: 33.33%" /&gt;&lt;col style="width: 33.33%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;P class="lia-align-left"&gt;&amp;nbsp;&lt;/P&gt;
&lt;P class="lia-align-left"&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;P class="lia-align-left"&gt;&amp;nbsp;&lt;/P&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H2&gt;&lt;SPAN class="lia-text-color-15"&gt;Test Scenario 2: Better P&lt;/SPAN&gt;&lt;SPAN class="lia-text-color-15"&gt;erformance at same price&lt;/SPAN&gt;&lt;/H2&gt;
&lt;/DIV&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P class="lia-align-left"&gt;At the same price point, Premium SSD v2 delivers higher throughput and lower latency than Premium SSD without requiring any application changes. To demonstrate this, we ran multiple pgbench tests using two workload configurations 8 clients / 8 threads and 32 clients / 32 threads with each run lasting 20 minutes. Results were consistent across all runs, with Premium SSD v2 consistently outperforming Premium SSD. &lt;EM&gt;Both configurations cost &lt;STRONG&gt;$578/month,&lt;/STRONG&gt; the only difference is storage performance.&lt;/EM&gt;&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN lia-align-left"&gt;&amp;nbsp;&lt;/DIV&gt;
&lt;img /&gt;
&lt;P class="lia-align-left"&gt;&amp;nbsp;&lt;/P&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H6&gt;&amp;nbsp;&lt;/H6&gt;
&lt;/DIV&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H6&gt;&amp;nbsp;&lt;/H6&gt;
&lt;/DIV&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H6&gt;&lt;SPAN class="lia-text-color-15"&gt;&lt;EM&gt;&lt;STRONG&gt;Results&lt;/STRONG&gt;:&lt;/EM&gt;&lt;/SPAN&gt;&lt;/H6&gt;
&lt;/DIV&gt;
&lt;P class="lia-align-left"&gt;&lt;STRONG&gt;Moderate concurrency (8 clients)&lt;/STRONG&gt;&lt;BR /&gt;Premium SSD v2 delivered approximately &lt;STRONG&gt;154% higher throughput (Transactions Per Second)&lt;/STRONG&gt; than Premium SSD (&lt;STRONG&gt;1,813 &lt;EM&gt;TPS &lt;/EM&gt;&lt;/STRONG&gt;vs. &lt;STRONG&gt;715 TPS&lt;/STRONG&gt;), while average latency decreased by about &lt;STRONG&gt;60%&lt;/STRONG&gt; (from &lt;STRONG&gt;~11.1 ms&lt;/STRONG&gt; to &lt;STRONG&gt;~4.4 ms&lt;/STRONG&gt;).&lt;/P&gt;
&lt;P class="lia-align-left"&gt;&lt;STRONG&gt;High concurrency (32 clients)&lt;/STRONG&gt;&lt;BR /&gt;The performance gap increases as concurrency grows, Premium SSD v2 delivered about &lt;STRONG&gt;169% higher throughput&lt;/STRONG&gt; than Premium SSD (&lt;STRONG&gt;3,643 TPS&lt;/STRONG&gt; vs. &lt;STRONG&gt;~1,352 TPS&lt;/STRONG&gt;) and reduced average latency by around &lt;STRONG&gt;67%&lt;/STRONG&gt; (from &lt;STRONG&gt;~26.3 ms&lt;/STRONG&gt; to &lt;STRONG&gt;~8.7 ms&lt;/STRONG&gt;).&lt;/P&gt;
&lt;P class="lia-align-left"&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;&lt;img /&gt;
&lt;P class="lia-align-left"&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/P&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H3&gt;&amp;nbsp;&lt;/H3&gt;
&lt;/DIV&gt;
&lt;P class="lia-align-left"&gt;&amp;nbsp;&lt;/P&gt;
&lt;P class="lia-align-left"&gt;&amp;nbsp;&lt;/P&gt;
&lt;P class="lia-align-left"&gt;&amp;nbsp;&lt;/P&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H3&gt;&amp;nbsp;&lt;/H3&gt;
&lt;/DIV&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H3&gt;&lt;SPAN class="lia-text-color-15"&gt;IOPS Behavior&lt;/SPAN&gt;&lt;/H3&gt;
&lt;/DIV&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;UL class="lia-align-left"&gt;
&lt;LI&gt;In the 8‑client, 8‑thread test, Premium SSD reached its IOPS ceiling early, operating at 100% utilization, while Premium SSD v2 retained approximately 30% headroom under the same workload delivering &lt;STRONG&gt;8,037 IOPS vs 3,761&lt;/STRONG&gt; IOPS with Premium SSD.&lt;/LI&gt;
&lt;LI&gt;When the workload increased to 32 clients and 32 threads, both tiers approached their IOPS limits however, Premium SSD v2 sustained a significantly higher performance ceiling, delivering approximately &lt;STRONG&gt;2.75x higher IOPS (13,620 vs. 4,968) &lt;/STRONG&gt;under load.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="lia-align-left"&gt;&lt;STRONG&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Key Takeaway:&lt;/STRONG&gt; With Premium SSD v2, you do not need to choose between cost and performance you get both. At the same price, applications&amp;nbsp; run faster, scale further, and maintain lower latency without any code changes.&lt;/P&gt;
&lt;P class="lia-align-left"&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H2&gt;&lt;SPAN class="lia-text-color-15"&gt;Migrate from Premium SSD to Premium SSD v2&lt;/SPAN&gt;&lt;/H2&gt;
&lt;/DIV&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P class="lia-align-left"&gt;Migrating is simple and fast. You can migrate from Premium SSD to Premium SSD v2 using the two strategies below with minimal downtime. These methods are generally quicker than logical migration strategies, such as exporting and restoring data using pg_dump and pg_restore.&lt;/P&gt;
&lt;UL class="lia-align-left"&gt;
&lt;LI&gt;&lt;A href="https://learn.microsoft.com/en-us/azure/postgresql/compute-storage/concepts-storage-migrate-ssd-to-ssd-v2?tabs=portal-restore-custom-point" target="_blank" rel="noopener"&gt;Restore from Premium SSD to Premium SSD v2&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="http://learn.microsoft.com/azure/postgresql/compute-storage/concepts-storage-replicate-ssd-to-ssd-v2" target="_blank" rel="noopener"&gt;Migrate using Read Replicas&lt;/A&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="lia-align-left"&gt;When migrating from Premium SSD to Premium SSD v2, using &lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/postgresql/read-replica/concepts-read-replicas-virtual-endpoints#using-virtual-endpoints-for-consistent-hostname-during-point-in-time-recovery-pitr-or-snapshot-restore" target="_blank" rel="noopener"&gt;a virtual endpoint&lt;/A&gt; helps keep downtime to a minimum and allows applications to continue operating without requiring configuration changes after the migration.&lt;/P&gt;
&lt;P class="lia-align-left"&gt;After the migration completes, you can stop the original server until your backup requirements are met. Once the required backup retention period has elapsed and all new backups are available on the new server, the original server can be safely deleted.&lt;/P&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H1&gt;&lt;SPAN class="lia-text-color-15"&gt;Region Availability &amp;amp; Features Supported&lt;/SPAN&gt;&lt;/H1&gt;
&lt;/DIV&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P class="lia-align-left"&gt;Premium SSD v2 is available in &lt;STRONG&gt;48 regions&lt;/STRONG&gt; worldwide for Azure Database for PostgreSQL – Flexible Server. For the most up‑to‑date information on regional availability, supported features, and current limitations, refer to the official Premium SSD v2 &lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/postgresql/compute-storage/concepts-storage-premium-ssd-v2#supported-features" target="_blank" rel="noopener"&gt;documentation&lt;/A&gt;.&lt;/P&gt;
&lt;DIV class="lia-align-left"&gt;
&lt;H2&gt;&lt;SPAN class="lia-text-color-15"&gt;Getting Started:&lt;/SPAN&gt;&lt;/H2&gt;
&lt;/DIV&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P class="lia-align-left"&gt;&amp;nbsp;To learn more, review the official &lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/postgresql/compute-storage/concepts-storage" target="_blank" rel="noopener"&gt;documentation&lt;/A&gt; for storage configuration available with Azure Database for PostgreSQL. Your feedback is important to us, have suggestions, ideas, or questions? We would love to hear from you:&amp;nbsp;&lt;A href="https://aka.ms/pgfeedback" target="_blank" rel="noopener"&gt;https://aka.ms/pgfeedback&lt;/A&gt;.&lt;/P&gt;</description>
      <pubDate>Tue, 07 Apr 2026 17:29:16 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/premium-ssd-v2-is-now-generally-available-for-azure-database-for/ba-p/4508445</guid>
      <dc:creator>kabharati</dc:creator>
      <dc:date>2026-04-07T17:29:16Z</dc:date>
    </item>
    <item>
      <title>Handling Unique Constraint Conflicts in Logical Replication</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/handling-unique-constraint-conflicts-in-logical-replication/ba-p/4507066</link>
      <description>&lt;P&gt;&lt;EM&gt;Authors: Ashutosh Sharma, Senior Software Engineer, and Gauri Kasar, Product Manager&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;Logical replication can keep your PostgreSQL environments in sync, helping replicate selected tables with minimal impact on the primary workload. But what happens when your subscriber hits a duplicate key error and replication grinds to a halt? If you’ve seen a &lt;A class="lia-external-url" href="https://www.postgresql.org/docs/current/logical-replication-conflicts.html" target="_blank" rel="noopener"&gt;unique‑constraint violation&lt;/A&gt; while replicating between Azure Database for PostgreSQL servers, you’re not alone. This blog covers common causes, prevention tips, and practical recovery options.&lt;/P&gt;
&lt;P&gt;In PostgreSQL logical replication, the subscriber can fail with a unique-constraint error when it tries to apply a change that would create a duplicate key.&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;duplicate key value violates unique constraint&lt;/LI-CODE&gt;
&lt;H2&gt;Understanding why this happens?&lt;/H2&gt;
&lt;P&gt;When an INSERT or UPDATE would create a value that already exists in a column (or set of columns) protected by a UNIQUE constraint (including a PRIMARY KEY). In logical replication, this most commonly occurs because of local writes on the subscriber or&amp;nbsp;if the table is being subscribed from multiple publishers. These conflicts are resolved on the subscriber side.&amp;nbsp;&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;&lt;STRONG&gt;Local writes on the subscriber&lt;/STRONG&gt;: a row with the same primary key/unique key is inserted on the subscriber before the apply worker processes the corresponding change from the publisher.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG style="color: rgb(30, 30, 30);"&gt;Multi-origin / multi-master without conflict-free keys&lt;/STRONG&gt;&lt;SPAN style="color: rgb(30, 30, 30);"&gt;: two origins generate (or replicate) the same unique key. &lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG style="color: rgb(30, 30, 30);"&gt;Initial data synchronization issues&lt;/STRONG&gt;&lt;SPAN style="color: rgb(30, 30, 30);"&gt;: the subscriber already contains data when the subscription is created with initial copy enabled, resulting in duplicate inserts during the initial table sync.&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;H2&gt;How to avoid this?&lt;/H2&gt;
&lt;UL&gt;
&lt;LI&gt;Avoid local writes on subscribed tables (treat the subscriber as read-only for replicated relations).&amp;nbsp;&lt;/LI&gt;
&lt;LI&gt;Avoid subscribing to the same table from multiple publishers unless you have explicit conflict handling and a conflict-free key design.&amp;nbsp;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Enabling server logs can help you identify and troubleshoot unique‑constraint conflicts more effectively. Refer to the official documentation &lt;A href="https://learn.microsoft.com/azure/postgresql/monitor/how-to-configure-and-access-logs?source=recommendations" target="_blank" rel="noopener"&gt;to configure and access PostgreSQL logs&lt;/A&gt;.&lt;/P&gt;
&lt;H2&gt;How to handle conflicts (recovery options)&amp;nbsp;&lt;/H2&gt;
&lt;H4&gt;Option 1: Delete the conflicting row on the subscriber&amp;nbsp;&lt;/H4&gt;
&lt;P&gt;Use the subscriber logs to identify the key (or row) causing the conflict, then delete the row on the subscriber with a DELETE statement. Resume apply and repeat if more conflicts appear.&amp;nbsp;&lt;/P&gt;
&lt;H4&gt;Option 2: Use conflict logs and skip the conflicting transaction (PostgreSQL 17+)&amp;nbsp;&lt;/H4&gt;
&lt;P&gt;Starting with PostgreSQL 17, logical replication provides &lt;STRONG&gt;detailed conflict logging&lt;/STRONG&gt; on the subscriber, making it easier to understand &lt;EM&gt;why&lt;/EM&gt; replication stopped and &lt;EM&gt;which transaction&lt;/EM&gt; caused the failure. When a replicated INSERT would violate a &lt;STRONG&gt;non‑deferrable unique constraint&lt;/STRONG&gt; on the subscriber for example, when a row with the same key already exists the apply worker detects this as an insert_exists conflict and stops replication. In this case, PostgreSQL logs the conflict along with the &lt;STRONG&gt;transaction’s finish LSN&lt;/STRONG&gt;, which uniquely identifies the failing transaction.&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;ERROR: conflict detected on relation "public.t2": conflict=insert_exists
... in transaction 754, finished at 0/034F4090
ALTER SUBSCRIPTION &amp;lt;subscription_name&amp;gt; SKIP (lsn = '0/034F4090');
&lt;/LI-CODE&gt;
&lt;H4&gt;Option 3: Rebuild (re-sync) the table&lt;/H4&gt;
&lt;P&gt;Rebuilding (re‑syncing) a table is the &lt;STRONG&gt;safest and most deterministic way&lt;/STRONG&gt; to resolve logical replication conflicts caused by &lt;STRONG&gt;pre‑existing data differences&lt;/STRONG&gt; or &lt;STRONG&gt;local writes on the subscriber&lt;/STRONG&gt;. This approach is especially useful when a table repeatedly fails with unique‑constraint violations and it is unclear which rows are out of sync.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Step 1 (subscriber):&lt;/STRONG&gt; Disable the subscription.&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;ALTER SUBSCRIPTION &amp;lt;subscription_name&amp;gt; DISABLE;&lt;/LI-CODE&gt;
&lt;P&gt;&lt;STRONG&gt;Step 2 (subscriber):&lt;/STRONG&gt;&amp;nbsp;Remove the local copy of the table so it can be re-copied.&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;TRUNCATE TABLE &amp;lt;conflicting_table&amp;gt;;&lt;/LI-CODE&gt;
&lt;P&gt;&lt;STRONG&gt;Step 3 (publisher):&lt;/STRONG&gt;&amp;nbsp;Ensure the publication will (re)send the table (one approach is to recreate the publication entry for that table).&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;ALTER PUBLICATION &amp;lt;pub_with_conflicting_table&amp;gt; DROP TABLE &amp;lt;conflicting_table&amp;gt;;
CREATE PUBLICATION &amp;lt;pub_with_conflicting_table_rebuild&amp;gt; FOR TABLE &amp;lt;conflicting_table&amp;gt;;
&lt;/LI-CODE&gt;
&lt;P&gt;&lt;STRONG&gt;Step 4 (subscriber):&lt;/STRONG&gt;&amp;nbsp;Create a new subscription (or refresh the existing one) to re-copy the table.&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;CREATE SUBSCRIPTION &amp;lt;sub_rebuild&amp;gt;
    CONNECTION '&amp;lt;connection_string&amp;gt;'
    PUBLICATION &amp;lt;pub_with_conflicting_table_rebuild&amp;gt;;
&lt;/LI-CODE&gt;
&lt;P&gt;&lt;STRONG&gt;Step 5 (subscriber):&lt;/STRONG&gt;&amp;nbsp;Re-enable the original subscription (if applicable).&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;ALTER SUBSCRIPTION &amp;lt;subscription_name&amp;gt; ENABLE;&lt;/LI-CODE&gt;
&lt;H3&gt;Conclusion&lt;/H3&gt;
&lt;P&gt;In most cases, these conflicts occur due to local changes on the subscriber or differences in data that existed before logical replication was fully synchronized. It is recommended to avoid direct modifications on subscribed tables and ensure that the replication setup is properly planned, especially when working with tables that have unique constraints.&lt;/P&gt;</description>
      <pubDate>Thu, 02 Apr 2026 16:33:58 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/handling-unique-constraint-conflicts-in-logical-replication/ba-p/4507066</guid>
      <dc:creator>gauri-kasar</dc:creator>
      <dc:date>2026-04-02T16:33:58Z</dc:date>
    </item>
    <item>
      <title>No code left behind: How AI streamlines Oracle-to-PostgreSQL migration</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/no-code-left-behind-how-ai-streamlines-oracle-to-postgresql/ba-p/4506107</link>
      <description>&lt;H5&gt;&lt;EM&gt;Coauthored by Jonathon Frost, Aditya Duvuri and Shriram Muthukrishnan&lt;/EM&gt;&lt;/H5&gt;
&lt;P&gt;More and more organizations are choosing PostgreSQL over proprietary database platforms such as Oracle, and for good reasons. It’s fully open source and community supported with a steady pace of innovation. It’s also preferred by developers for its extensibility and flexibility, often being used for vector data along with relational data to support modern applications and agents. Still, organizations considering a shift from Oracle to PostgreSQL, may hesitate due to the complexity that often accompanies an enterprise-scale migration project. Challenges such as incompatible data types, language mismatches, and the risk of breaking critical applications are hard to ignore.&lt;/P&gt;
&lt;P&gt;Recently, the &lt;A href="https://www.youtube.com/watch?v=LdCExagKS4Y" target="_blank" rel="noopener"&gt;Azure Postgres team released a new, free tool for migrations from Oracle to PostgreSQL&lt;/A&gt; that was designed to address these challenges, making the decision to migrate a lot less risky. The new AI-assisted Oracle-to-PostgreSQL migration tool, available in public preview via the &lt;A href="https://marketplace.visualstudio.com/items?itemName=ms-ossdata.vscode-pgsql" target="_blank" rel="noopener"&gt;PostgreSQL extension for Visual Studio Code&lt;/A&gt;, brings automation, validation, and AI-powered migration assistance into a single, user-friendly interface.&lt;/P&gt;
&lt;H2&gt;Meet your new migration assistant&lt;/H2&gt;
&lt;P&gt;The AI-assisted Oracle to PostgreSQL migration tool dramatically simplifies moving off Oracle databases. Accessible through VS Code, the tool uses intelligent automation, powered by GitHub Copilot, to convert Oracle database schemas and PL/SQL code into PostgreSQL-compatible formats. It can analyze Oracle schema, and automatically translate table definitions, data types, and even stored procedures/triggers into PostgreSQL equivalents speeding up migrations that once took months of manual effort.&lt;/P&gt;
&lt;P&gt;By handling the heavy lifting of schema and code conversion, this tool allows teams to focus on higher-level testing and optimization rather than tedious code rewrites. Users are already reporting that migrations are now faster, safer, and more transparent. The tool is simple, free, and ready for you to use today. Let’s take a look at how it works by covering the following:&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;Creating the migration project&lt;/LI&gt;
&lt;LI&gt;Setting up the connections&lt;/LI&gt;
&lt;LI&gt;AI-assisted schema migration&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt; &lt;/STRONG&gt;Reviewing schema migration report&lt;/LI&gt;
&lt;LI&gt;AI-assisted application migration&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt; &lt;/STRONG&gt;Reviewing application migration report&lt;/LI&gt;
&lt;/OL&gt;
&lt;H2&gt;Step by step with the AI-assisted Oracle-to-PostgreSQL migration tool&lt;/H2&gt;
&lt;H3&gt;Step 1 – Create the project in VS Code&lt;/H3&gt;
&lt;P&gt;Start by installing or updating the PostgreSQL extension for VS Code from the marketplace. Open the PostgreSQL extension panel and click “Create Migration Project.” You’ll name your project, which will create a folder to store all migration artifacts. This folder will house extracted and converted files, organized for version control and collaboration.&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;H3&gt;Step 2 - Connect to your databases and AI model&lt;/H3&gt;
&lt;P&gt;Before beginning the migration, you’ll need to connect to the Oracle databases and select an OpenAI model to leverage during the process. Enter the connection details for your source Oracle database, credentials, and the schema to migrate. Then, select a PostgreSQL scratch database. This temporary environment is used to validate converted DDL in real time. Next, you will be prompted to select an OpenAI model.&lt;/P&gt;
&lt;H3&gt;Step 3 – Begin schema migration&lt;/H3&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;Once you’ve set up your connections, click the button to start the schema migration. The tool performs an extraction of all relevant Oracle database objects: tables, views, packages, procedures, and more. The extracted DDL is saved as files in your project folder. This file-based approach functions like a software project, enabling change tracking, collaboration, and source control.&lt;/P&gt;
&lt;H4&gt;Enter - AI assistance&lt;/H4&gt;
&lt;P&gt;This is where the AI takes over. The tool breaks the extracted schema into manageable chunks, and each chunk is processed by a multi-agent orchestration system:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;The Migration Specialist Agent converts Oracle DDL to PostgreSQL.&lt;/LI&gt;
&lt;LI&gt;The Migration Critic Agent validates the conversion by executing it in the PostgreSQL scratch database.&lt;/LI&gt;
&lt;LI&gt;The Documentation Agent captures follow up review tasks, metadata, and coding notes for later integration with the application code migration process.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Each chunk is converted, validated, and deployed. If validation fails, the agents auto correct and retry. This self-healing loop ensures high conversion accuracy. Essentially, the tool conducts compile-time validation against a live PostgreSQL instance to catch issues early and reduce downstream surprises.&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;H3&gt;Checkpoint - review the schema migration report&lt;/H3&gt;
&lt;P&gt;Some complex objects, like Oracle packages with intricate PL/SQL, may not convert cleanly on the first pass. These are flagged as “review tasks.” You can invoke GitHub Copilot’s agent mode directly from VS Code to assist. The tool constructs a composite prompt with the original Oracle DDL, the partially converted PostgreSQL version, and any validation errors. This context-rich prompt enables Copilot to generate more accurate fixes.&lt;/P&gt;
&lt;P&gt;With the schema fully converted, you can compare the original Oracle and new PostgreSQL versions side by side. Right-click any object in the project folder and select “Compare File Pair." You can also use the “Visualize Schema” feature to see a graphical representation of the converted schema. This is ideal for verifying tables, relationships, and constraints.&lt;/P&gt;
&lt;P&gt;Once the schema migration is complete, the tool generates a detailed report that includes:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Total number of objects converted&lt;/LI&gt;
&lt;LI&gt;Conversion success rate&lt;/LI&gt;
&lt;LI&gt;PostgreSQL version and extensions used&lt;/LI&gt;
&lt;LI&gt;List of converted objects by type&lt;/LI&gt;
&lt;LI&gt;Any flagged review tasks&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;This report serves as both a validation summary and an audit artifact. It helps confirm success and identify any follow-up actions. If you have compliance or change management requirements you need to meet, this documentation is essential.&lt;/P&gt;
&lt;H3&gt;Step 4 – Begin application migration&lt;/H3&gt;
&lt;P&gt;The next phase that the tool supports is updating the application code that interacts with the schema. Migrations often stall when code is overlooked or when traditional tools treat SQL statements as simple strings rather than part of a cohesive system. The AI-assisted Oracle-to-PostgreSQL migration tool’s application conversion feature takes a more holistic, context-aware approach.&lt;/P&gt;
&lt;P&gt;Before starting, you’ll need to configure GitHub Copilot Agent Mode with a capable AI model. Then, navigate to the ‘application_code’ directory typically found in &lt;EM&gt;.github/postgres-migration/&amp;lt;project_name&amp;gt;/application_code&lt;/EM&gt;, and copy your source code into this directory. Keeping your application and converted schema together provides the AI with the structural context it needs to refactor your code accurately. To start the app migration, this time you’d select the "Migrate Application" button. Then select the folder containing your source code and the converted schema.&lt;/P&gt;
&lt;H4&gt;Enter - AI assistance&lt;/H4&gt;
&lt;P&gt;The AI orchestrator will analyze your application’s database interactions against the new Postgres schema and generate a series of transformation tasks. These tasks address SQL dialect changes, data access modifications, and library updates. This process goes beyond a simple search-and-replace operation. The AI queries your migrated PostgreSQL database to gain grounded context of your converted schema, and ensures that things like function signatures, data types, and ORM models are migrated correctly in the application code.&lt;/P&gt;
&lt;H3&gt;Checkpoint - review the app migration report&lt;/H3&gt;
&lt;P&gt;When the AI finishes converting your application, it produces a detailed summary. The report lists which files were migrated, notes any unresolved tasks, and outlines how the changes map to the database schema. This audit-ready document can help DBAs and developers collaborate effectively on follow-up actions and integration testing.&lt;/P&gt;
&lt;P&gt;You can use VS Code’s built-in diff viewer to compare each migrated file with its original. Right-click on a migrated file and select "Compare App Migration File Pairs" to open a side-by-side view. This comparison highlights differences in SQL queries, driver imports, and other code changes, allowing you to verify the updates.&lt;/P&gt;
&lt;H3&gt;Wrapping up the migration project&lt;/H3&gt;
&lt;P&gt;During schema migration, the tool created detailed coding notes summarizing data-type mappings, constraints, and package transformations. These notes are essential for understanding why specific changes were made and for guiding the application conversion. Use them as reference points when validating and refining the AI-generated application code.&lt;/P&gt;
&lt;H3&gt;Destination - PostgreSQL on Azure&lt;/H3&gt;
&lt;P&gt;The AI-assisted Oracle-to-PostgreSQL migration tool brings together automation, validation, and AI to make Oracle-to-PostgreSQL migrations faster, safer, and more transparent. With schema extraction, multi-agent orchestration, app conversion, real-time validation, and detailed reporting, it provides a clear, confident path to modernization so you can start taking advantage of the benefits of open-source Postgres.&lt;/P&gt;
&lt;H4&gt;What’s in store&lt;/H4&gt;
&lt;P&gt;On the other side of a successful migration project to PostgreSQL on Azure, you get:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;First-class support in Azure&lt;/LI&gt;
&lt;LI&gt;Significantly lower total cost of ownership from eliminating license fees and reducing vendor lock-in&lt;/LI&gt;
&lt;LI&gt;Unmatched extensibility, with support for custom data types, procedural languages and powerful extensions like PostGIS, TimescaleDB, pgvector, Azure AI, and DiskANN&lt;/LI&gt;
&lt;LI&gt;Frequent updates and cutting-edge features delivered via a vibrant open-source community&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Whether you’re migrating a single schema or leading a broader replatforming initiative, the AI-assisted Oracle-to-PostgreSQL migration tool helps you move forward with confidence without sacrificing control or visibility.&lt;/P&gt;
&lt;P&gt;&lt;A class="lia-external-url" href="https://learn.microsoft.com/en-us/azure/postgresql/migrate/oracle-schema-conversions/schema-conversions-overview" target="_blank" rel="noopener"&gt;Learn more about starting your own migration project.&lt;/A&gt;&lt;/P&gt;</description>
      <pubDate>Tue, 31 Mar 2026 15:00:00 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/no-code-left-behind-how-ai-streamlines-oracle-to-postgresql/ba-p/4506107</guid>
      <dc:creator>TeneilLawrence</dc:creator>
      <dc:date>2026-03-31T15:00:00Z</dc:date>
    </item>
    <item>
      <title>PostgreSQL Buffer Cache Analysis</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/postgresql-buffer-cache-analysis/ba-p/4501264</link>
      <description>&lt;P&gt;PostgreSQL performance is often dictated not just by query design or indexing strategy, but by how effectively the database leverages memory. At the heart of this memory usage lies shared_buffers—PostgreSQL’s primary buffer cache. Understanding how well this cache is utilized can make the difference between a system that scales smoothly and one that struggles under load.&lt;/P&gt;
&lt;P&gt;In this post, we’ll walk you through a practical, data-driven approach to analyzing PostgreSQL buffer cache behavior using native statistics and the pg_buffercache extension. The goal is to answer a few critical questions:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Is the current shared_buffers configuration sufficient?&lt;/LI&gt;
&lt;LI&gt;Are high-value tables and indexes actually being served from memory?&lt;/LI&gt;
&lt;LI&gt;Is PostgreSQL spending too much time going to disk when it shouldn’t?&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;By the end, you’ll have a repeatable methodology to assess cache efficiency and make informed tuning decisions.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Why Buffer Cache Analysis Matters&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;PostgreSQL relies heavily on its buffer cache to minimize disk I/O. Every time a query needs a data or index page, PostgreSQL first checks whether that page already exists in shared_buffers. If it does, the page is served directly from memory—fast and efficient. If not, PostgreSQL must fetch it from disk (or the OS page cache), which is significantly slower.&lt;/P&gt;
&lt;P&gt;While metrics like query latency and IOPS can tell you that performance is degraded, buffer cache analysis helps explain why. It allows you to:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Validate whether frequently accessed objects stay hot in cache&lt;/LI&gt;
&lt;LI&gt;Identify cache pollution caused by large, low-value tables&lt;/LI&gt;
&lt;LI&gt;Determine whether increasing shared_buffers would provide real benefits or just waste memory&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;Inspecting Shared Buffers with pg_buffercache&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;The pg_buffercache extension provides a real-time view into PostgreSQL’s shared buffers. Unlike cumulative statistics, it shows what is in memory right now—which relations are cached, how many blocks they occupy, and how frequently those buffers are reused.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Enabling the Extension&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;pg_buffercache is not enabled by default and requires superuser privileges:&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;CREATE EXTENSION pg_buffercache;&lt;/LI-CODE&gt;
&lt;P&gt;Once enabled, you can directly query the contents of shared buffers across databases, tables, and indexes.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Analyzing Cache Distribution&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Understanding where your shared buffers are being consumed is the first step toward meaningful tuning.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Database-Level Cache Distribution&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;This query shows how shared buffers are distributed across databases in the server:&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;SELECT CASE
  WHEN c.reldatabase IS NULL THEN ''
  WHEN c.reldatabase = 0 THEN ''
  ELSE d.datname
END AS database,
count(*) AS cached_blocks
FROM pg_buffercache AS c
LEFT JOIN pg_database AS d ON c.reldatabase = d.oid
WHERE datname NOT LIKE 'template%'
GROUP BY d.datname, c.reldatabase
ORDER BY d.datname, c.reldatabase;&lt;/LI-CODE&gt;
&lt;P&gt;This is particularly useful in multi-database environments where one workload may be evicting cache pages needed by another.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Table and Index-Level Cache Consumption&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;To understand which relations, dominate the cache, the following query breaks buffer usage down by tables and indexes:&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;SELECT c.relname, c.relkind, count(*)
FROM pg_database AS a, pg_buffercache AS b, pg_class AS c
WHERE c.relfilenode = b.relfilenode
AND b.reldatabase = a.oid
GROUP BY 1, 2
ORDER BY 3 DESC, 1;&lt;/LI-CODE&gt;
&lt;P&gt;This helps answer an important question: Are your most business-critical tables and indexes actually resident in memory, or are they constantly being evicted?&lt;/P&gt;
&lt;P&gt;If large, rarely used tables consume a disproportionate share of buffers, it may indicate cache churn or the need for workload isolation.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Understanding Buffer Usage Count (Hot vs Cold Data)&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Each buffer in shared memory carries a usage count, which reflects how frequently it has been accessed before eviction. Higher values indicate hotter data.&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;SELECT c.relname, c.relkind, usagecount, count(*) AS buffers
FROM pg_database AS a, pg_buffercache AS b, pg_class AS c
WHERE c.relfilenode = b.relfilenode
AND b.reldatabase = a.oid
AND a.datname = current_database()
GROUP BY 1, 2, 3
ORDER BY 3 DESC, 1;&lt;/LI-CODE&gt;
&lt;P&gt;A healthy system typically shows a meaningful number of buffers with higher usage counts (for example, 4–5), indicating frequently reused data that benefits from caching.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Buffer Cache Percentages: Putting Numbers in Context&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Raw buffer counts are useful, but percentages make interpretation easier. The following query shows:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;How much of shared_buffers each relation occupies&lt;/LI&gt;
&lt;LI&gt;What percentage of the relation itself is cached&lt;/LI&gt;
&lt;/UL&gt;
&lt;LI-CODE lang="sql"&gt;SELECT c.relname,
pg_size_pretty(count(*) * 8192) AS buffered,
round(100.0 * count(*) / (SELECT setting FROM pg_settings WHERE name='shared_buffers')::integer, 1) AS buffers_percent,
round(100.0 * count(*) * 8192 / pg_relation_size(c.oid), 1) AS percent_of_relation
FROM pg_class c
JOIN pg_buffercache b ON b.relfilenode = c.relfilenode
JOIN pg_database d ON b.reldatabase = d.oid AND d.datname = current_database()
GROUP BY c.oid, c.relname
ORDER BY 3 DESC
LIMIT 10;&lt;/LI-CODE&gt;
&lt;P&gt;This view is especially powerful when validating whether performance-critical objects are adequately cached relative to their size.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Complementing Cache Views with I/O Statistics&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;While pg_buffercache shows the current state of memory, I/O statistics reveal long-term trends. PostgreSQL exposes these via pg_statio_user_tables and pg_statio_user_indexes.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Table Heap Hit Ratios&lt;/STRONG&gt;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;SELECT relname,
heap_blks_hit::numeric / (heap_blks_hit + heap_blks_read) AS hit_pct,
heap_blks_hit,
heap_blks_read
FROM pg_catalog.pg_statio_user_tables
WHERE (heap_blks_hit + heap_blks_read) &amp;gt; 0
ORDER BY hit_pct;&lt;/LI-CODE&gt;
&lt;P&gt;Hit ratios close to 1 indicate that table data is largely served from memory rather than disk.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Index Hit Ratios&lt;/STRONG&gt;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;SELECT relname,
idx_blks_hit::numeric / (idx_blks_hit + idx_blks_read) AS hit_pct,
idx_blks_hit,
idx_blks_read
FROM pg_catalog.pg_statio_user_tables
WHERE (idx_blks_hit + idx_blks_read) &amp;gt; 0
ORDER BY hit_pct;&lt;/LI-CODE&gt;
&lt;P&gt;Poor index hit ratios often point to insufficient cache or inefficient query patterns that bypass indexes.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Including TOAST and Index Reads&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;For large objects, TOAST activity can significantly impact I/O. This query provides a more holistic view:&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;SELECT *,
(heap_blks_read + toast_blks_read + tidx_blks_read) AS total_blocks_read,
(heap_blks_hit + toast_blks_hit + tidx_blks_hit) AS total_blocks_hit
FROM pg_catalog.pg_statio_user_tables;&lt;/LI-CODE&gt;
&lt;P&gt;This helps identify indexes that are frequently read from disk and may benefit from better caching or query rewrites.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;How to Interpret the Results&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;When reviewing buffer cache and I/O metrics, keep the following guidelines in mind:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Validate cache residency of critical objects&lt;/STRONG&gt;: If business-critical tables and indexes occupy a meaningful share of shared_buffers, your cache sizing is likely reasonable.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Correlate buffer data with hit ratios:&lt;/STRONG&gt; High hit ratios in pg_statio_user_tables and pg_statio_user_indexes confirm effective caching. Persistently low ratios may justify increasing shared_buffers.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Analyze usage count distribution:&lt;/STRONG&gt; A healthy number of buffers with higher usage counts indicates hot data benefiting from cache reuse.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Avoid over-tuning&lt;/STRONG&gt;: If most buffers have low usage counts but hit ratios remain high, increasing shared_buffers further may not yield measurable gains.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;Conclusion&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Buffer cache analysis bridges the gap between theory and reality in PostgreSQL performance tuning. By combining real-time cache inspection with long-term I/O statistics, you gain a clear picture of how memory is actually used—and whether changes to shared_buffers will deliver tangible benefits.&lt;/P&gt;
&lt;P&gt;Rather than tuning memory blindly, this approach lets you optimize with confidence, grounded in data that reflects your real workload.&lt;/P&gt;</description>
      <pubDate>Tue, 31 Mar 2026 12:51:22 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/postgresql-buffer-cache-analysis/ba-p/4501264</guid>
      <dc:creator>Gayathri_Paderla</dc:creator>
      <dc:date>2026-03-31T12:51:22Z</dc:date>
    </item>
    <item>
      <title>Bidirectional Replication with pglogical on Azure Database for PostgreSQL - a VNET guide</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/bidirectional-replication-with-pglogical-on-azure-database-for/ba-p/4506319</link>
      <description>&lt;P&gt;&lt;STRONG&gt;Editor’s Note&lt;/STRONG&gt;: This article was written by &lt;STRONG&gt;Raunak Jhawar&lt;/STRONG&gt;, a Chief Architect. Paula Berenguel and Guy Bowerman assisted with the final review, formatting and publication.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Overview&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Bidirectional replication is one of the most requested topologies requiring writes in multiple locations, selective sync, geo-distributed active-active, or even accepting eventual consistency.&lt;/P&gt;
&lt;P&gt;This is a deep technical walkthrough for implementing bidirectional (active‑active) replication on private Azure Database for PostgreSQL Server using pglogical, with a strong emphasis on VNET‑injected architectures. It explains the underlying networking and execution model covering replication worker placement, DNS resolution paths, outbound connectivity, and conflict resolution mechanics to show why true private, server‑to‑server replication is only achievable with VNET injection and not with Private Endpoints. It also analyzes the operational and architectural trade‑offs needed to safely run geo distributed, multi write PostgreSQL workloads in production.&lt;/P&gt;
&lt;P&gt;This blog post focus on pglogical however, if you are looking for steps to implement it with logical replication or pros and cons of which approach, please refer to my definitive guid to bi-directional replication in Azure Database for PostgreSQL&amp;nbsp;&lt;A class="lia-internal-link lia-internal-url lia-internal-url-content-type-blog" href="https://techcommunity.microsoft.com/blog/adforpostgresql/the-definitive-guide-to-bi-directional-replication-in-azure-database-for-postgre/4450550" target="_blank" rel="noopener" data-lia-auto-title="blog post" data-lia-auto-title-active="0"&gt;blog post&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Why this is important?&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;This understanding prevents fundamental architectural mistakes (such as assuming Private Endpoints provide private outbound replication), reduces deployment failures caused by hidden networking constraints, and enables teams to design secure, compliant, low‑RPO active/active or migration architectures that behave predictably under real production conditions. It turns a commonly misunderstood problem into a repeatable, supportable design pattern rather than a trial‑and‑error exercise.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Active-Active bidirectional replication between instances&amp;nbsp;&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Architecture context&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;This scenario targets a multi-region active-active write topology where both nodes are injected into the same Azure VNET (example - peered VNETs on Azure or even peered on-premises), both accept writes.&lt;/P&gt;
&lt;P&gt;Common use case: Geo distributed OLTP with regional write affinity.&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Step 1: Azure Infrastructure Prerequisites&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Both server instances must be deployed with VNET injection. This is a deploy time decision and you cannot migrate a publicly accessible instance (with or without private endpoint) to VNET injection post creation without rebuilding it.&lt;/P&gt;
&lt;P&gt;Each instance must live in a delegated subnet: Microsoft.DBforPostgreSQL/Servers. The subnet delegation is non-negotiable and prevents you from placing other resource types in the same subnet, so plan your address space accordingly.&lt;/P&gt;
&lt;P&gt;If nodes are in different VNETs, configure VNET peering before continuing along with private DNS integration. Ensure there are no overlapping address spaces amongst the peered networks.&lt;/P&gt;
&lt;P&gt;NSG rules must allow port 5432 between the two delegated subnets, both inbound and outbound. You may choose to narrow down the NSG rules to meet your organization requirements and policies to a specific source/target combination allow or deny list.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Step 2: Server Parameter Configuration&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;On both nodes, configure the following server parameters via the Azure Portal (Server Parameters blade) or Azure CLI. These cannot be set via ALTER SYSTEM SET commands.&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;wal_level = logical -- This setting enables logical replication, which is required for pglogical to function.&lt;/P&gt;
&lt;P&gt;max_worker_processes = 16 -- This setting allows for more worker processes, which can help with replication performance.&lt;/P&gt;
&lt;P&gt;max_replication_slots = 10 -- This setting allows for more replication slots, which are needed for pglogical to manage replication connections.&lt;/P&gt;
&lt;P&gt;max_wal_senders = 10 -- This setting allows for more WAL sender processes, which are responsible for sending replication data to subscribers.&lt;/P&gt;
&lt;P&gt;track_commit_timestamp = on -- This setting allows pglogical to track commit timestamps, which can be useful for conflict resolution and monitoring replication lag.&lt;/P&gt;
&lt;P&gt;shared_preload_libraries = pglogical -- This setting loads the pglogical extension at server startup, which is necessary for it to function properly.&lt;/P&gt;
&lt;P&gt;azure.extensions = pglogical -- This setting allows the pglogical extension to be used in the Azure Postgres PaaS environment.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;EM&gt;Both nodes require a restart after shared_preload_libraries and wal_level changes. &lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;&lt;EM&gt;Note that max_worker_processes is shared across all background workers in the instance. Each pglogical subscription consumes workers. If you are running other extensions, account for their worker consumption here or you will hit startup failures for pglogical workers.&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Step 3: Extension and Node Initialization&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Create a dedicated replication user on both nodes. Do not use the admin account for replication.&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE ROLE replication_user WITH LOGIN REPLICATION PASSWORD 'your_password';&lt;/P&gt;
&lt;P&gt;GRANT USAGE ON SCHEMA public TO replication_user;&lt;/P&gt;
&lt;P&gt;GRANT SELECT ON ALL TABLES IN SCHEMA public TO replication_user;&lt;/P&gt;
&lt;P&gt;ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO replication_user;&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P&gt;Log into Server A either via a VM in the specified VNET or Azure Bastion Host and run the following which creates the extension, a replication set and policies.&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;CREATE EXTENSION IF NOT EXISTS pglogical;&lt;/P&gt;
&lt;P&gt;SELECT pglogical.create_node(node_name := 'node_a', dsn := 'host.fqdn-for-server-a port=5432 dbname=preferred-database user=replication_user password=&amp;lt;strong_password&amp;gt;');&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;-- Define the replication set for Server A, specifying which tables to replicate and the types of operations to include (inserts, updates, deletes).&lt;/P&gt;
&lt;P&gt;SELECT pglogical.create_replication_set(set_name := 'node_a_set',&amp;nbsp; replicate_insert := true,&amp;nbsp; replicate_update := true,&amp;nbsp; replicate_delete := true,&amp;nbsp; replicate_truncate := false);&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;-- Add sales_aus_central table explicitly&lt;/P&gt;
&lt;P&gt;SELECT pglogical.replication_set_add_table(set_name := 'node_a_set',&amp;nbsp; relation := 'public.sales_aus_central',&amp;nbsp; synchronize_data := true);&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;-- Add purchase_aus_central table explicitly&lt;/P&gt;
&lt;P&gt;SELECT pglogical.replication_set_add_table(set_name := 'node_a_set',&amp;nbsp; relation := 'public.purchase_aus_central',&amp;nbsp; synchronize_data := true);&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;-- OR add all tables in the public schema&lt;/P&gt;
&lt;P&gt;SELECT pglogical.replication_set_add_all_tables('default', ARRAY['public']); -- This command adds all tables in the public schema to the default replication set.&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;-- Now, repeat this on Server B using the same method above i.e. via a VM in the specified VNET or Azure Bastion Host&lt;/P&gt;
&lt;P&gt;CREATE EXTENSION IF NOT EXISTS pglogical;&lt;/P&gt;
&lt;P&gt;-- Define the replication set for Server B, specifying which tables to replicate and the types of operations to include (inserts, updates, deletes)&lt;/P&gt;
&lt;P&gt;SELECT pglogical.create_node(node_name := 'node_b', dsn := 'host-fqdn-for-server-b port=5432 dbname=preferred-database user=replication_user password=&amp;lt;strong_password&amp;gt;');&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;SELECT pglogical.create_replication_set(&amp;nbsp; set_name := 'node_b_set',&amp;nbsp; replicate_insert := true,&amp;nbsp; replicate_update := true,&amp;nbsp; replicate_delete := true,&lt;/P&gt;
&lt;P&gt;&amp;nbsp; replicate_truncate := false);&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;-- Add sales_aus_east table explicitly&lt;/P&gt;
&lt;P&gt;SELECT pglogical.replication_set_add_table(&amp;nbsp; set_name := 'node_b_set',&amp;nbsp; relation := 'public.sales_aus_east',&amp;nbsp; synchronize_data := true);&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;-- Add purchase_aus_east table explicitly&lt;/P&gt;
&lt;P&gt;SELECT pglogical.replication_set_add_table(&amp;nbsp; set_name := 'node_b_set',&amp;nbsp; relation := 'public.purchase_aus_east',&amp;nbsp; synchronize_data := true);&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;-- OR add all tables in the public schema&lt;/P&gt;
&lt;P&gt;SELECT pglogical.replication_set_add_all_tables('default', ARRAY['public']); -- This command adds all tables in the public schema to the default replication set.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P&gt;It is recommended that you confirm the DNS resolution on all server’s involved as part of the replication process. For a VNET injected scenarios – you must get back the private IP.&lt;/P&gt;
&lt;P&gt;As a sanity check, you can run the nslookup on the target server’s FQDN or even use the \conninfo command to see the connection details. One such example is here:&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Step 4: Configuring the subscribers&lt;/STRONG&gt;&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;SELECT pglogical.create_subscription ( -- Create a subscription on Server A to receive changes from Server B&lt;/P&gt;
&lt;P&gt;subscription_name := 'node_a_to_node_b',&lt;/P&gt;
&lt;P&gt;replication_sets := array['default'],&lt;/P&gt;
&lt;P&gt;synchronize_data := true,&lt;/P&gt;
&lt;P&gt;forward_origins := '{}',&lt;/P&gt;
&lt;P&gt;provider_dsn := 'host=fqdn-for-server-b port=5432 dbname=preferred-database user=replication_user password=&amp;lt;strong_password&amp;gt;');&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;-- Run this on Server B to subscribe to changes from Server A&lt;/P&gt;
&lt;P&gt;SELECT pglogical.create_subscription ( -- Create a subscription on Server B to receive changes from Server A&lt;/P&gt;
&lt;P&gt;subscription_name := 'node_b_to_node_a',&lt;/P&gt;
&lt;P&gt;replication_sets := array['default'],&lt;/P&gt;
&lt;P&gt;synchronize_data := true,&lt;/P&gt;
&lt;P&gt;forward_origins := '{}',&lt;/P&gt;
&lt;P&gt;provider_dsn := 'host=fqdn-for-server-a port=5432 dbname=preferred-database user=replication_user password=&amp;lt;strong_password&amp;gt;');&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;EM&gt;For most OLTP workloads, last_update_wins using the commit timestamp is the most practical choice. It requires track_commit_timestamp = on, which you must set as a server parameter.&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;&lt;EM&gt;The FQDN must be used rather than using the direct private IP of the server itself.&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Bidirectional replication between server instances with private endpoints – does this work and will this make your server security posture weak?&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Where do pglogical workers run?&lt;/P&gt;
&lt;P&gt;With VNET injection, the server's network interface lives inside your delegated subnet which is a must do. The PostgreSQL process including all pglogical background workers starts connections from within your VNET (delegated subnet). The routing tables, NSGs, and peering apply to both inbound &lt;EM&gt;and&lt;/EM&gt; outbound traffic from the server.&lt;/P&gt;
&lt;P&gt;With Private Endpoint, the architecture is fundamentally different:&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;Private endpoint is a one-way private channel for your clients or applications to reach the server securely. It does not give the any of server’s internal processes access to your VNET for outbound connectivity.&lt;/P&gt;
&lt;P&gt;pglogical subscription workers trying to connect to another server are starting those connections from Microsoft's managed infrastructure and not from your VNET.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;What works?&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Scenario A: Client connectivity via private endpoint&lt;/P&gt;
&lt;P&gt;Here you have application servers or VMs in your VNET connecting to a server configured with a private endpoint, your app VM connects to 10.0.0.15 (the private endpoint NIC), traffic flows over Private Link to the server, and everything stays private. This is not server-to-server replication.&lt;/P&gt;
&lt;P&gt;Scenario B: Two servers, both with private endpoints&lt;/P&gt;
&lt;P&gt;Here both servers are in Microsoft's managed network. They can reach each other's public endpoints, but not each other's private endpoints (which are in customer VNETs). The only path for bidirectional replication worker connections is to enable public network access on both servers with firewall rules locked down to Azure service IP.&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;Here you have private endpoints deployed alongside public access. Inside your VNET, SERVER A resolves to the private endpoint IP via the privatelink.postgres.database.azure.com private DNS zone. But the pglogical worker running in Microsoft's network does not have access to your private DNS zone and it resolves via public DNS, which returns the public IP.&lt;/P&gt;
&lt;P&gt;This means if you are using the public FQDN for replication, the resolution path is consistent from the server's perspective (always public DNS, always public IP using the allow access to Azure services flag as shown above). Your application clients in the VNET will still resolve to the private endpoint.&lt;/P&gt;
&lt;P&gt;If your requirement is genuinely private replication with no public endpoint exposure, VNET injection is the correct answer, and private endpoint cannot replicate that capability for pglogical.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Conclusion&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;The most compelling benefit in the VNET-injected topology is network isolation without sacrificing replication capability. You get the security posture of private connectivity i.e. no public endpoints, NSG controlled traffic, private DNS resolution all while keeping a live bidirectional data pipeline. This satisfies most enterprise compliance requirements around data transit encryption and network boundary control.&lt;/P&gt;
&lt;P&gt;The hub/spoke migration (specifically, on-premises or external cloud to Azure) scenarios are where this approach shines. The ability to run both systems in production simultaneously, with live bidirectional sync during the cutover window, reduces migration risk when compared to a hard cutover.&lt;/P&gt;
&lt;P&gt;From a DR perspective, bidirectional pglogical gives you an RPO measured in seconds (replication lag dependent) without the cost of synchronous replication. For workloads that can tolerate eventual consistency and have well-designed conflict avoidance this is a compelling alternative to synchronous streaming replication via read replicas, which are strictly unidirectional.&lt;/P&gt;</description>
      <pubDate>Mon, 30 Mar 2026 15:30:26 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/bidirectional-replication-with-pglogical-on-azure-database-for/ba-p/4506319</guid>
      <dc:creator>pberenguel</dc:creator>
      <dc:date>2026-03-30T15:30:26Z</dc:date>
    </item>
    <item>
      <title>PostgreSQL Query Rewriting and Subqueries</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/postgresql-query-rewriting-and-subqueries/ba-p/4499819</link>
      <description>&lt;P&gt;In PostgreSQL, the way a query is written has a direct impact on the execution plan the optimizer selects—and consequently on the overall performance of the system. Although the optimizer evaluates multiple plan alternatives, it can only consider strategies that the SQL structure logically allows. Suboptimal query patterns often force PostgreSQL into performing unnecessary work, such as processing more rows than needed, repeating lookups row‑by‑row or applying expensive deduplication or aggregation operations after an overly large join.&lt;/P&gt;
&lt;P&gt;By rewriting queries in more optimizer‑friendly forms, we can drastically reduce the amount of data scanned, improve join efficiency, eliminate redundant operations, and enable PostgreSQL to choose faster, more scalable execution paths. The scenarios in this document demonstrate how simple transformations—such as replacing joins with semi‑joins, pre‑aggregating large tables, or converting correlated subqueries into set‑based joins—allow PostgreSQL to leverage more efficient physical operators and achieve significant performance gains.&lt;/P&gt;
&lt;P&gt;This document explains common PostgreSQL scenarios, where rewriting a query into a subquery (or rewriting a subquery into a different form) can significantly improve performance. Each scenario includes an explanation, example rewrite, and a self-contained test script you can run.&lt;/P&gt;
&lt;P&gt;The below test was performed on a 4 vcore SKU with 128 GB storage disk.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Test setup/ script:&lt;/STRONG&gt;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS customers;

CREATE TABLE customers (
  customer_id int PRIMARY KEY,
  region text NOT NULL
);

CREATE TABLE orders (
  order_id bigserial PRIMARY KEY,
  customer_id int NOT NULL REFERENCES customers(customer_id),
  created_at timestamptz NOT NULL
);

INSERT INTO customers
SELECT g, CASE WHEN g % 5 = 0 THEN 'E' ELSE 'W' END
FROM generate_series(1,200000) g;

INSERT INTO orders(customer_id, created_at)
SELECT 1 + ((g*37) % 200000), now() - ((g % 365) || ' days')::interval
FROM generate_series(1,5000000) g;

CREATE INDEX orders_customer_id_idx ON orders(customer_id);

ANALYZE customers;
ANALYZE orders;&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;--------------------------------------------------&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Scenario 1: Semi-join filtering (JOIN → EXISTS)&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;--------------------------------------------------&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;When it helps:&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Use this when you only need to check for the existence of related rows and do not need columns from the joined table. EXISTS allows PostgreSQL to use a semi-join strategy and avoids row multiplication.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Before:&lt;/STRONG&gt;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;SELECT DISTINCT c.customer_id
FROM customers c
JOIN orders o ON o.customer_id = c.customer_id
WHERE c.region = 'E';&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Query plan:&lt;/STRONG&gt;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&lt;STRONG&gt;Explanation:&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;This query forces PostgreSQL to:&lt;BR /&gt;• Perform a &lt;STRONG&gt;full join&lt;/STRONG&gt; between customers and orders&lt;BR /&gt;• Produce &lt;STRONG&gt;multiple rows per customer&lt;/STRONG&gt; (one for each matching order)&lt;BR /&gt;• Apply a &lt;STRONG&gt;Unique/Aggregate&lt;/STRONG&gt; step to remove duplicates&lt;/P&gt;
&lt;P&gt;This results in:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;More rows flowing through the join&lt;/LI&gt;
&lt;LI&gt;Higher memory usage&lt;/LI&gt;
&lt;LI&gt;An expensive deduplication step&lt;/LI&gt;
&lt;LI&gt;Possible large hash tables or repeated nested loop iterations&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;After:&lt;/STRONG&gt;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;SELECT c.customer_id
FROM customers c
WHERE c.region = 'E'
  AND EXISTS (
    SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id
  );&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Query plan:&lt;/STRONG&gt;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&lt;STRONG&gt;Explanation:&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;The rewritten query using EXISTS, PostgreSQL can switch to a &lt;STRONG&gt;Semi Join&lt;/STRONG&gt;, which has two crucial advantages:&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;&lt;STRONG&gt;Stops scanning orders after the first match&lt;/STRONG&gt;&lt;BR /&gt;A semi‑join checks only for the &lt;EM&gt;existence&lt;/EM&gt; of at least one matching row — it does not need all of them. Early termination dramatically reduces I/O.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;No need for DISTINCT&lt;/STRONG&gt;&lt;BR /&gt;Because no row multiplication occurs, the result is naturally unique.&lt;/LI&gt;
&lt;/OL&gt;
&lt;P&gt;&lt;STRONG&gt;&amp;nbsp;&lt;/STRONG&gt;&lt;STRONG&gt;Why performance improves&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Fewer rows scanned&lt;/LI&gt;
&lt;LI&gt;No row explosion&lt;/LI&gt;
&lt;LI&gt;No sort/aggregate for DISTINCT&lt;/LI&gt;
&lt;LI&gt;Semi‑join is cheaper both in CPU and memory&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;Net effect:&lt;/STRONG&gt; Large reduction in join work and downstream processing.&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;--------------------------------------------------&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Scenario 2: Pre-aggregate using a subquery&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;--------------------------------------------------&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;When it helps:&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;If you only need aggregated results from a large fact table, aggregate first in a subquery and then join. This reduces join cardinality and memory usage.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Before:&lt;/STRONG&gt;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;SELECT c.region, count(*) AS order_ct
FROM customers c
JOIN orders o ON o.customer_id = c.customer_id
GROUP BY c.region;&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Query plan:&lt;/STRONG&gt;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&lt;STRONG&gt;Explanation:&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;The original query aggregates at the end, &lt;STRONG&gt;after joining&lt;/STRONG&gt; the entire orders table with customers.&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;The join processes millions of rows&lt;/LI&gt;
&lt;LI&gt;Grouping is done on the &lt;STRONG&gt;expanded&lt;/STRONG&gt; join result&lt;/LI&gt;
&lt;LI&gt;Memory consumption grows significantly&lt;/LI&gt;
&lt;LI&gt;Hash join must handle a very large input&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;After:&lt;/STRONG&gt;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;SELECT c.region, sum(x.order_ct) AS order_ct
FROM customers c
JOIN (
  SELECT customer_id, count(*) AS order_ct
  FROM orders
  GROUP BY customer_id
) x ON x.customer_id = c.customer_id
GROUP BY c.region;&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Query plan:&lt;/STRONG&gt;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&lt;STRONG&gt;Explanation:&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;The rewritten query aggregates orders &lt;EM&gt;before&lt;/EM&gt; joining&lt;/P&gt;
&lt;P&gt;This reduces the orders table from millions of rows to just thousands (one per customer). PostgreSQL can now join a &lt;STRONG&gt;tiny, aggregated set&lt;/STRONG&gt; with customers.&lt;/P&gt;
&lt;P&gt;PostgreSQL’s plan changes accordingly:&lt;BR /&gt;• &lt;STRONG&gt;HashAggregate&lt;/STRONG&gt; on orders (small output)&lt;BR /&gt;• Join happens on significantly reduced data&lt;BR /&gt;• Final grouping is trivial because input has already been summarized&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Why performance improves&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Drastic reduction in join input size&lt;/LI&gt;
&lt;LI&gt;Much smaller hash tables&lt;/LI&gt;
&lt;LI&gt;Fewer rows grouped at the final stage&lt;/LI&gt;
&lt;LI&gt;Less disk spill risk&lt;/LI&gt;
&lt;LI&gt;Better CPU and memory efficiency&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;Net effect:&lt;/STRONG&gt; Doing heavy work early shrinks the workload for the rest of the query.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;--------------------------------------------------&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Scenario 3: Correlated scalar subquery → JOIN&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;--------------------------------------------------&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;When it helps:&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Correlated scalar subqueries can behave like N+1 queries. Rewriting them as joins allows PostgreSQL to use more efficient join strategies.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Before:&lt;/STRONG&gt;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;SELECT o.order_id,
       (SELECT c.region FROM customers c WHERE c.customer_id = o.customer_id) AS region
FROM orders o;&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Query plan:&lt;/STRONG&gt;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&lt;STRONG&gt;Explanation:&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Correlated Scalar Subquery&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;A correlated subquery runs &lt;STRONG&gt;once per row&lt;/STRONG&gt; of the outer table (orders).&lt;/P&gt;
&lt;P&gt;PostgreSQL has no choice but to generate a &lt;STRONG&gt;parameterized Nested Loop&lt;/STRONG&gt;, which means:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Scan one order&lt;/LI&gt;
&lt;LI&gt;Perform an index lookup into customers&lt;/LI&gt;
&lt;LI&gt;Repeat for every order&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;The performance degrades linearly with table size.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;After:&lt;/STRONG&gt;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;SELECT o.order_id, c.region
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id;
&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Query plan:&lt;BR /&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&lt;STRONG&gt;Explanation:&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;The rewritten query is a standard join removes the row‑by‑row dependency. Now PostgreSQL can:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Scan both tables once&lt;/LI&gt;
&lt;LI&gt;Build a hash table on customers&lt;/LI&gt;
&lt;LI&gt;Use a &lt;STRONG&gt;Hash Join&lt;/STRONG&gt; or &lt;STRONG&gt;Merge Join&lt;/STRONG&gt; to match rows efficiently&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;This transforms the execution pattern from row‑by‑row lookups to a &lt;STRONG&gt;set‑based join&lt;/STRONG&gt;, which is far more efficient.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Why performance improves&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Eliminates repeated index probes&lt;/LI&gt;
&lt;LI&gt;Replaces O(N) loops with a single O(N) scan + O(N) hash join&lt;/LI&gt;
&lt;LI&gt;Better cache utilization&lt;/LI&gt;
&lt;LI&gt;Fully parallelizable (semi‑correlated loops are not)&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;Net effect:&lt;/STRONG&gt; Orders and customers are processed together in a single, optimized join rather than tens or hundreds of thousands of micro‑queries.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Conclusion:&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;The rewritten queries enable PostgreSQL to choose far more efficient physical operations—such as semi‑joins, early aggregation, and set‑based hash/merge joins—dramatically reducing row processing, memory usage, and repetitive work. Overall, these improvements streamline execution paths, allowing the optimizer to operate on smaller, cleaner datasets and produce faster, more scalable query plans.&lt;/P&gt;</description>
      <pubDate>Thu, 26 Mar 2026 20:40:00 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/postgresql-query-rewriting-and-subqueries/ba-p/4499819</guid>
      <dc:creator>Gayathri_Paderla</dc:creator>
      <dc:date>2026-03-26T20:40:00Z</dc:date>
    </item>
    <item>
      <title>GraphRAG and PostgreSQL integration in docker with Cypher query and AI agents (Version 2*)</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/graphrag-and-postgresql-integration-in-docker-with-cypher-query/ba-p/4503586</link>
      <description>&lt;P&gt;&lt;STRONG&gt;This is update from previous blog (version 1):&amp;nbsp;&lt;A class="lia-internal-link lia-internal-url lia-internal-url-content-type-blog" href="https://techcommunity.microsoft.com/blog/azure-ai-foundry-blog/graphrag-and-postgresql-integration-in-docker-with-cypher-query-and-ai-agents/4420623" target="_blank" rel="noopener" data-lia-auto-title="GraphRAG and PostgreSQL integration in docker with Cypher query and AI agents | Microsoft Community Hub" data-lia-auto-title-active="0"&gt;GraphRAG and PostgreSQL integration in docker with Cypher query and AI agents | Microsoft Community Hub&lt;/A&gt;&lt;/STRONG&gt;&lt;/P&gt;
&lt;H2&gt;&lt;STRONG&gt;Review the business needs of this solution from version 1&lt;/STRONG&gt;&lt;/H2&gt;
&lt;img /&gt;
&lt;H2&gt;&lt;STRONG&gt;What's new in version 2?&lt;/STRONG&gt;&lt;/H2&gt;
&lt;H2&gt;&lt;STRONG&gt;MCP tools for GraphRAG and PostgreSQL with Apache AGE&amp;nbsp;&lt;/STRONG&gt;&lt;/H2&gt;
&lt;UL&gt;
&lt;LI&gt;This solution now includes MCP tools for GraphRAG and PostgreSQL. There are five MCP tools exposed:&amp;nbsp;&lt;BR /&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;[graphrag_search]&lt;/STRONG&gt;&amp;nbsp;&lt;BR /&gt;Used to run query (local or global) with runtime-tunable API parameters. One important aspect is that query behavior can be tuned at runtime, without changing the underlying index.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;[age_get_schema_cached]&lt;/STRONG&gt;&lt;BR /&gt;Used for schema inspection and diagnostics. It returns the graph schema (node labels and relationship types) from cache by default; and can optionally refresh the cache by re‑querying the database. This tool is typically used for introspection or debugging, not for answering user questions about data.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;[age_entity_lookup]&lt;/STRONG&gt;&lt;BR /&gt;Used for quick entity discovery and disambiguation. It performs a simple substring match on entity names or titles and is especially useful for questions like&amp;nbsp;&lt;EM&gt;“Who is X?”&lt;/EM&gt;&amp;nbsp;or as a preliminary step before issuing more complex graph queries.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;[age_cypher_query]&lt;/STRONG&gt;&lt;BR /&gt;Executes a user‑provided Cypher query directly against the AGE graph. This is intended for advanced users who already know the graph structure and want full control over traversal logic and filters.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;[age_nl2cypher_query]&lt;/STRONG&gt;&lt;BR /&gt;Bridges natural language and Cypher. This tool converts a natural‑language question into a Cypher query (using only Entity nodes and RELATED_TO edges), executes it, and returns the results. It is most effective for multi‑hop or structurally complex questions where semantic interpretation is needed first, but execution must remain deterministic.&lt;/LI&gt;
&lt;/UL&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Besides that,&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;This solution now uses Microsoft agent framework. It enables clean orchestration over MCP tools, allowing the agent to dynamically select between GraphRAG and graph query capabilities at runtime, with a looser coupling and clearer execution model than traditional Semantic Kernel function plugins.&lt;/LI&gt;
&lt;LI&gt;The new Docker image includes graphRAG3.0.5. This version stabilizes the 3.x configuration‑driven, API‑based architecture and improves indexing reliability, making graph construction more predictable and easier to integrate into real workflows.&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;&lt;STRONG&gt;New architecture&lt;/STRONG&gt;&lt;/H2&gt;
&lt;img /&gt;
&lt;H2&gt;&lt;STRONG&gt;Updated Step 7 - run query in Jupyter notebook&lt;/STRONG&gt;&lt;/H2&gt;
&lt;P&gt;This step runs Jupyter notebook in docker, which is the same as stated in previous blog.&amp;nbsp;&lt;BR /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;gt; docker compose up query-notebook&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;After clicking the link highlighted in the above screen shot, you can explore all files within the project in the docker, then find the &lt;EM&gt;&lt;U&gt;query-notebook.ipynb&lt;/U&gt;&lt;/EM&gt;.&lt;/P&gt;
&lt;P&gt;&lt;A class="lia-external-url" href="https://github.com/Azure-Samples/postgreSQL-graphRAG-docker/blob/main/project_folder/query-notebook.ipynb" target="_blank" rel="noopener"&gt;https://github.com/Azure-Samples/postgreSQL-graphRAG-docker/blob/main/project_folder/query-notebook.ipynb&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;But in this new version of notebook, the graphRAG3.0.5 uses different library for local Search and global Search.&lt;/P&gt;
&lt;H2&gt;&lt;STRONG&gt;New Step 8 - run agent and MCP tools in Jupyter notebook&lt;/STRONG&gt;&lt;/H2&gt;
&lt;P&gt;This step runs Jupyter notebook in docker.&amp;nbsp;&lt;BR /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;gt; docker compose up mcp-agent&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;Click on the highlighted URL, you can start working on&lt;U&gt;&amp;nbsp;&lt;EM&gt;agent-notebook.ipynb&lt;/EM&gt;&lt;/U&gt;. &amp;nbsp;&lt;BR /&gt;&lt;A class="lia-external-url" href="https://github.com/Azure-Samples/postgreSQL-graphRAG-docker/blob/main/project_folder/agent-notebook.ipynb" target="_blank" rel="noopener"&gt;https://github.com/Azure-Samples/postgreSQL-graphRAG-docker/blob/main/project_folder/agent-notebook....&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;Multiple scenarios of agents with MCP tools are included in the notebook:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;GraphRAG search: local search and global search examples with direct mcp call.&lt;/LI&gt;
&lt;LI&gt;GraphRAG search: local search and global search examples with agent and include mcp tools.&lt;/LI&gt;
&lt;LI&gt;Cypher query in direct mcp call.&lt;/LI&gt;
&lt;LI&gt;Agent to query in natural language, and mcp tool included to convert NL2Cypher.&lt;/LI&gt;
&lt;LI&gt;Agent with unified mcp (all five mcp tools), and based on the question route to the corresponding tool.
&lt;UL&gt;
&lt;LI&gt;['graphrag_search', 'age_get_schema_cached', 'age_cypher_query', 'age_entity_lookup', 'age_nl2cypher_query']&lt;/LI&gt;
&lt;/UL&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;&lt;STRONG&gt;Router agent: selecting the right MCP tool&lt;/STRONG&gt;&lt;/H2&gt;
&lt;P&gt;The notebook also includes a&amp;nbsp;&lt;STRONG&gt;router agent&lt;/STRONG&gt;&amp;nbsp;that has access to all five MCP tools and decides which one to invoke based on the user’s question. Rather than hard‑coding execution paths, the agent reasons about intent and selects the most appropriate capability at runtime.&lt;/P&gt;
&lt;H3&gt;&lt;STRONG&gt;General routing guidance used in this solution&lt;/STRONG&gt;&lt;/H3&gt;
&lt;P&gt;&lt;STRONG&gt;Use [graphrag_search]&lt;/STRONG&gt; when the question requires:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;full dataset understanding,&lt;/LI&gt;
&lt;LI&gt;themes, patterns, or trends across documents,&lt;/LI&gt;
&lt;LI&gt;exploratory or open‑ended analysis,&lt;/LI&gt;
&lt;LI&gt;global understanding or evaluation where we have a corpus of many tokens.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;In these cases, GraphRAG’s semantic retrieval and aggregation are a better fit than explicit graph traversal.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Use AGE‑based tools&lt;/STRONG&gt;&lt;BR /&gt;[age_get_schema_cached, age_entity_lookup, age_cypher_query, age_nl2cypher_query] when the question involves:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;specific entities or explicit relationships,&lt;/LI&gt;
&lt;LI&gt;deterministic graph traversal or filtering,&lt;/LI&gt;
&lt;LI&gt;questions that depend on graph structure rather than document semantics,&lt;/LI&gt;
&lt;LI&gt;complex graph queries involving multiple entities or multi‑hop paths.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Within the AGE toolset:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;[age_entity_lookup] is typically used for quick entity discovery or disambiguation.&lt;/LI&gt;
&lt;LI&gt;[age_cypher_query] is used when a precise Cypher query is already known.&lt;/LI&gt;
&lt;LI&gt;[age_nl2cypher_query] is used when the question is expressed in natural language but requires a non‑trivial Cypher query to answer.&lt;/LI&gt;
&lt;LI&gt;[age_get_schema_cached] is reserved for schema inspection and diagnostics.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;The router agent dynamically selects between semantic search and deterministic graph tools based on question intent, keeping retrieval, graph execution, and orchestration clearly separated and extensible.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Note:&lt;/STRONG&gt;&amp;nbsp;The repository also includes [age_get_schema] and [age_get_schema_details] MCP tools for debugging and development purposes. These are not exposed to agents by default and are superseded by [age_get_schema_cached] for normal use.&lt;/P&gt;
&lt;H2&gt;&lt;STRONG&gt;Key takeaways&lt;/STRONG&gt;&lt;/H2&gt;
&lt;UL&gt;
&lt;LI&gt;GraphRAG and postgreSQL AGE querying serve different purposes and each has its advantages.&lt;/LI&gt;
&lt;LI&gt;MCP tools provide a&amp;nbsp;uniform interface to both semantic search and deterministic graph operations.&lt;/LI&gt;
&lt;LI&gt;Microsoft Agent Framework enables&amp;nbsp;tool‑centric orchestration, where agents select the right capability at runtime instead of hard‑coding logic in prompts.&lt;/LI&gt;
&lt;LI&gt;The Jupyter‑based agent workflow makes it easy to experiment with different interaction patterns, from direct tool calls to fully routed agent execution.&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;&lt;STRONG&gt;What's next&lt;/STRONG&gt;&lt;/H2&gt;
&lt;UL&gt;
&lt;LI&gt;In this solution, the MCP server and agent runtime are architecturally separated but deployed together in a single Docker container to demonstrate how MCP tools work and to keep local experimentation simple.&lt;/LI&gt;
&lt;LI&gt;There are other deployment options, such as running MCP servers remotely, where tools can be hosted and operated independently of the agent runtime. Contributions and enhancements are welcome.&lt;/LI&gt;
&lt;/UL&gt;</description>
      <pubDate>Mon, 23 Mar 2026 12:08:09 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/graphrag-and-postgresql-integration-in-docker-with-cypher-query/ba-p/4503586</guid>
      <dc:creator>Helen_Zeng</dc:creator>
      <dc:date>2026-03-23T12:08:09Z</dc:date>
    </item>
    <item>
      <title>February 2026 Recap: Azure Database for PostgreSQL</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/february-2026-recap-azure-database-for-postgresql/ba-p/4501093</link>
      <description>&lt;P&gt;Hello Azure Community,&lt;/P&gt;
&lt;P&gt;We’re excited to share the February 2026 recap for &lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/postgresql/" target="_blank" rel="noopener"&gt;Azure Database for PostgreSQL&lt;/A&gt;, featuring a set of updates focused on speed, simplicity, and better visibility. From Terraform support for Elastic Clusters and a refreshed VM SKU selection experience in the Azure portal to built‑in Grafana dashboards, these improvements make it easier to build, operate, and scale PostgreSQL on Azure. This recap also includes practical GIN index tuning guidance, enhancements to the PostgreSQL VS Code extension, and improved connectivity for azure_pg_admin users.&lt;/P&gt;
&lt;H1&gt;Features&lt;/H1&gt;
&lt;OL&gt;
&lt;LI style="font-weight: bold;"&gt;&lt;STRONG&gt;&lt;A href="#community--1-tf" target="_self" rel="noopener"&gt;Terraform support for Elastic Clusters - Generally Available&lt;/A&gt;&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI style="font-weight: bold;"&gt;&lt;STRONG&gt;&lt;A href="#community--1-dashboard" target="_self" rel="noopener"&gt;Dashboards with Grafana - Generally Available&lt;/A&gt;&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI style="font-weight: bold;"&gt;&lt;STRONG&gt;&lt;A href="#community--1-sku" target="_self" rel="noopener"&gt;Easier way to choose VM SKUs on portal – Generally Available&lt;/A&gt;&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI style="font-weight: bold;"&gt;&lt;STRONG&gt;&lt;A href="#community--1-vscode" target="_self" rel="noopener"&gt;What’s New in the PostgreSQL VS Code Extension&lt;/A&gt;&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI style="font-weight: bold;"&gt;&lt;STRONG&gt;&lt;A href="#community--1-admin" target="_self" rel="noopener"&gt;Priority Connectivity to azure_pg_admin users&lt;/A&gt;&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI style="font-weight: bold;"&gt;&lt;STRONG&gt;&lt;A href="#community--1-gin" target="_self" rel="noopener"&gt;Guide on 'gin_pending_list_limit' indexes&lt;/A&gt;&lt;/STRONG&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;H2 id="tf"&gt;Terraform support for Elastic Clusters&lt;/H2&gt;
&lt;P&gt;Terraform now supports provisioning and managing Azure Database for PostgreSQL Elastic Clusters, enabling customers to define and operate elastic clusters using infrastructure‑as‑code workflows. With this support, it is now easier to create, scale, and manage multi‑node PostgreSQL clusters through Terraform, making it easier to automate deployments, replicate environments, and integrate elastic clusters into CI/CD pipelines. This improves operational consistency and simplifies management for horizontally scalable PostgreSQL workloads.&lt;/P&gt;
&lt;P&gt;Learn more about building and scaling with &lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/postgresql/elastic-clusters/concepts-elastic-clusters" target="_blank" rel="noopener"&gt;Azure Database for PostgreSQL elastic clusters.&lt;/A&gt;&lt;/P&gt;
&lt;H2 id="dashboard"&gt;Dashboards with Grafana — Now Built-In&lt;/H2&gt;
&lt;img /&gt;
&lt;P&gt;Grafana dashboards are now natively integrated into the Azure Portal for Azure Database for PostgreSQL. This removes the need to deploy or manage a separate Grafana instance. With just a few clicks, you can visualize key metrics and logs side by side, correlate events by timestamp, and gain deep insights into performance, availability, and query behavior all in one place.&lt;/P&gt;
&lt;P&gt;Whether you're troubleshooting a spike, monitoring trends, or sharing insights with your team, this built-in experience simplifies day-to-day observability with no added cost or complexity.&lt;/P&gt;
&lt;P&gt;Try it under &lt;STRONG&gt;Azure Portal &amp;gt; Dashboards with Grafana&lt;/STRONG&gt; in your PostgreSQL server view.&lt;/P&gt;
&lt;P&gt;For more details, see the &lt;A href="https://aka.ms/azure-postgres-dashboards-grafana" target="_blank" rel="noopener"&gt;blog post: &lt;EM&gt;Dashboards with Grafana — Now in Azure Portal for PostgreSQL&lt;/EM&gt;&lt;/A&gt;.&lt;/P&gt;
&lt;H2 id="sku"&gt;Easier way to choose VM SKUs on portal&lt;/H2&gt;
&lt;P&gt;&lt;SPAN data-teams="true"&gt;We’ve improved the VM SKU selection experience in the Azure portal to make it easier to find and compare the right compute options for your PostgreSQL workload. The updated experience organizes SKUs in a clearer, more scannable view, helping you quickly compare key attributes like vCores and memory without extra clicks. This streamlined approach reduces guesswork and makes selecting the right SKU faster and more intuitive.&lt;/SPAN&gt;&lt;/P&gt;
&lt;A href="&amp;gt;&amp;lt;/h2&amp;gt;
&amp;lt;h2 id=" target="_blank" rel="noopener"&gt;
&lt;H2 id="vscode"&gt;What’s New in the PostgreSQL VS Code Extension&lt;/H2&gt;
&lt;/A&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;A href="&amp;gt;&amp;lt;/h2&amp;gt;
&amp;lt;h2 id=" target="_blank" rel="noopener"&gt;The&amp;nbsp;&lt;/A&gt;&lt;A href="https://marketplace.visualstudio.com/items?itemName=ms-ossdata.vscode-pgsql" target="_blank" rel="noopener"&gt;VS Code extension for PostgreSQL&lt;/A&gt; helps developers and database administrators work with PostgreSQL directly from VS Code. It provides capabilities for querying, schema exploration, diagnostics, and Azure PostgreSQL management allowing users to stay within their editor while building and troubleshooting. &lt;A class="lia-external-url" href="https://github.com/microsoft/vscode-pgsql/blob/main/CHANGELOG.md" target="_blank" rel="noopener"&gt;This release focuses&lt;/A&gt; on improving developer productivity and diagnostics. It introduces new visualization capabilities, Copilot-powered experiences, enhanced schema navigation, and deeper Azure PostgreSQL management directly from VS Code.&lt;/P&gt;
&lt;H4&gt;New Features &amp;amp; Enhancements&lt;/H4&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Query Plan Visualization:&lt;/STRONG&gt; Graphical execution plans can now be viewed directly in the editor, making it easier to diagnose slow queries without leaving VS Code.&amp;nbsp;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;AGE Graph Rendering:&lt;/STRONG&gt; Support is now available for automatically rendering graph visualizations from Cypher queries, improving the experience of working with graph data in PostgreSQL.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Object Explorer Search: &lt;/STRONG&gt;A new graphical search experience in Object Explorer allows users to quickly find tables, views, functions, and other objects across large schemas, addressing one of the highest-rated user feedback requests.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Azure PostgreSQL Backup Management:&lt;/STRONG&gt; Users can now manage Azure Database for PostgreSQL backups directly from the Server Dashboard, including listing backups and configuring retention policies.&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Server Logs Dashboard:&lt;/STRONG&gt; A new Server Dashboard view surfaces Azure Database for PostgreSQL server logs and retention settings for faster diagnostics. Logs can be opened directly in VS Code and analyzed using the built-in GitHub Copilot integration.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;This release also includes several reliability improvements and bug fixes, including resolving connection pool exhaustion issues, fixing Docker container creation failures when no password is provided, and improving stability around connection profiles and schema-related operations.&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2 id="admin"&gt;Priority Connectivity to azure_pg_admin Users&lt;/H2&gt;
&lt;P&gt;Members of the azure_pg_admin role can now use connections from the pg_use_reserved_connections pool. This ensures that an admin always has at least one available connection, even if all standard client connections from the server connection pool are in use. By making sure admin users can log in when the client connection pool is full, this change prevents lockout situations and lets admins handle emergencies without competing for available open connection slots.&lt;/P&gt;
&lt;H2 id="gin"&gt;Guide on 'gin_pending_list_limit' indexes&lt;/H2&gt;
&lt;P&gt;Struggling with slow GIN index inserts in PostgreSQL? This post dives into the often-overlooked &lt;EM&gt;gin_pending_list_limit&lt;/EM&gt; parameter and how it directly impacts insert performance. Learn how GIN’s pending list works, why the right limit matters, and practical guidance on tuning it to strike the perfect balance between write performance and index maintenance overhead.&lt;/P&gt;
&lt;P&gt;For a deeper dive into &lt;EM&gt;gin_pending_list_limit&lt;/EM&gt; and tuning guidance, see the full blog &lt;A class="lia-internal-link lia-internal-url lia-internal-url-content-type-blog" href="https://techcommunity.microsoft.com/blog/adforpostgresql/mastering-gin-pending-list-limit-how-this-parameter-shapes-gin-index-insert-perf/4494203" target="_blank" rel="noopener" data-lia-auto-title="here" data-lia-auto-title-active="0"&gt;here&lt;/A&gt;.&lt;/P&gt;
&lt;H1&gt;Learning Bytes&lt;/H1&gt;
&lt;P&gt;Create Azure Database for PostgreSQL elastic clusters with terraform:&lt;/P&gt;
&lt;P&gt;Elastic clusters in Azure Database for PostgreSQL let you scale PostgreSQL horizontally using a managed, multi‑node architecture. With Elastic cluster now generally available, you can provision and manage elastic clusters using infrastructure‑as‑code, making it easier to automate deployments, standardize environments, and integrate PostgreSQL into CI/CD workflows.&lt;/P&gt;
&lt;P&gt;Elastic clusters are a good fit when you need:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Horizontal scale for large or fast‑growing PostgreSQL workloads&lt;/LI&gt;
&lt;LI&gt;Multi‑tenant applications or sharded data models&lt;/LI&gt;
&lt;LI&gt;Repeatable and automated deployments across environments&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;The following example shows a basic Terraform configuration to create an Azure Database for PostgreSQL flexible server configured as an elastic cluster.&lt;/P&gt;
&lt;LI-CODE lang="shell"&gt;resource "azurerm_postgresql_flexible_server" "elastic_cluster" {
  name                   = "pg-elastic-cluster"
  resource_group_name    = &amp;lt;rg-name&amp;gt;
  location               = &amp;lt;region&amp;gt;
  administrator_login    = var.admin_username
  administrator_password = var.admin_password
  version                = "17"
  sku_name   = "GP_Standard_D4ds_v5"
  storage_mb = 131072
  cluster {
    size = 3
  }
}&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;H1&gt;Conclusion&lt;/H1&gt;
&lt;P&gt;That’s a wrap for the February 2026 Azure Database for PostgreSQL recap. We’re continuing to focus on making PostgreSQL on Azure easier to build, operate, and scale whether that’s through better automation with Terraform, improved observability, or a smoother day‑to‑day developer and admin experience. Your feedback is important to us, have suggestions, ideas, or questions? We’d love to hear from you:&amp;nbsp;&lt;A href="https://aka.ms/pgfeedback" target="_blank" rel="noopener"&gt;https://aka.ms/pgfeedback&lt;/A&gt;.&lt;/P&gt;</description>
      <pubDate>Mon, 16 Mar 2026 01:25:38 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/february-2026-recap-azure-database-for-postgresql/ba-p/4501093</guid>
      <dc:creator>gauri-kasar</dc:creator>
      <dc:date>2026-03-16T01:25:38Z</dc:date>
    </item>
    <item>
      <title>Understanding Hash Join Memory Usage and OOM Risks in PostgreSQL</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/understanding-hash-join-memory-usage-and-oom-risks-in-postgresql/ba-p/4500308</link>
      <description>&lt;H2&gt;Background: Why Memory Usage May Exceed work_mem&lt;/H2&gt;
&lt;P&gt;work_mem is commonly assumed to be a hard upper bound on per‑query memory usage.&lt;/P&gt;
&lt;P&gt;However, for Hash Join operations, memory consumption depends not only on this parameter but also on:&lt;/P&gt;
&lt;P&gt;✅ Data cardinality&lt;/P&gt;
&lt;P&gt;✅ Hash table internal bucket distribution&lt;/P&gt;
&lt;P&gt;✅ Join column characteristics&lt;/P&gt;
&lt;P&gt;✅ Number of batches created&lt;/P&gt;
&lt;P&gt;✅ Parallel workers involved&lt;/P&gt;
&lt;P&gt;Under low‑cardinality conditions, a Hash Join may place an extremely large number of rows into very few buckets—sometimes a single bucket. This causes unexpectedly large memory allocations that exceed the nominal work_mem limit.&lt;/P&gt;
&lt;H2&gt;Background: What work_mem&amp;nbsp;&lt;EM&gt;really&lt;/EM&gt; means for Hash Joins&lt;/H2&gt;
&lt;UL&gt;
&lt;LI&gt;work_mem controls the amount of memory available &lt;STRONG&gt;per operation&lt;/STRONG&gt; (e.g., a sort or a hash) &lt;STRONG&gt;per node&lt;/STRONG&gt; (and per parallel worker) before spilling to disk. Hash operations can additionally use hash_mem_multiplier×work_mem for their hash tables. &lt;A href="https://www.postgresql.org/docs/current/runtime-config-resource.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;, &lt;A href="https://postgresqlco.nf/doc/en/param/hash_mem_multiplier/" target="_blank" rel="noopener"&gt;[postgresqlco.nf]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;The &lt;STRONG&gt;Hash Join&lt;/STRONG&gt; algorithm builds a hash table for the “build/inner” side and probes it with the “outer” side. The table is split into &lt;STRONG&gt;buckets&lt;/STRONG&gt;; if it doesn’t fit in memory, PostgreSQL partitions work into &lt;STRONG&gt;batches&lt;/STRONG&gt; (spilling to temporary files). &lt;STRONG&gt;Skewed distributions&lt;/STRONG&gt; (e.g., very few distinct join keys) pack many rows into the same bucket(s), exploding memory usage even when work_mem is small. &lt;A href="https://postgrespro.com/blog/pgsql/5969673" target="_blank" rel="noopener"&gt;[postgrespro.com]&lt;/A&gt;, &lt;A href="https://www.interdb.jp/pg/pgsql03/05/03.html" target="_blank" rel="noopener"&gt;[interdb.jp]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;In EXPLAIN (ANALYZE) you’ll see &lt;STRONG&gt;Buckets:&lt;/STRONG&gt;, &lt;STRONG&gt;Batches:&lt;/STRONG&gt;, and &lt;STRONG&gt;Memory Usage:&lt;/STRONG&gt; on the Hash node; Batches &amp;gt; 1 indicates spilling/partitioning. &lt;A href="https://www.postgresql.org/docs/current/using-explain.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;, &lt;A href="https://thoughtbot.com/blog/reading-an-explain-analyze-query-plan" target="_blank" rel="noopener"&gt;[thoughtbot.com]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;The default for hash_mem_multiplier is &lt;STRONG&gt;version‑dependent&lt;/STRONG&gt; (introduced in PG13; 1.0 in early versions, later 2.0). Tune with care; it scales the memory that hash operations may consume relative to work_mem. &lt;A href="https://pgpedia.info/h/hash_mem_multiplier.html" target="_blank" rel="noopener"&gt;[pgpedia.info]&lt;/A&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2&gt;A safe, reproducible demo (containerized community PostgreSQL)&lt;/H2&gt;
&lt;P&gt;The goal is to show that &lt;STRONG&gt;data distribution alone&lt;/STRONG&gt; can drive &lt;STRONG&gt;order(s) of magnitude&lt;/STRONG&gt; difference in hash table memory, using conservative settings.&lt;/P&gt;
&lt;P&gt;In order to simulate the behavior we´ll use &lt;STRONG&gt;pg_hint_plan&lt;/STRONG&gt; extension to guide the execution plans and create some data distribution that may not have a good application logic, just to force and show the behavior.&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;
&lt;H4&gt;Start PostgreSQL 16 container&lt;/H4&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;LI-CODE lang="bash"&gt;docker run --name=postgresql16.8 -p 5414:5432 -e POSTGRES_PASSWORD=&amp;lt;password&amp;gt; -d postgres:16.8
docker exec -it postgresql16.8 /bin/bash -c "apt-get update -y;apt-get install procps -y;apt-get install postgresql-16-pg-hint-plan -y;apt-get install vim -y;apt-get install htop -y"
docker exec -it postgresql16.8 /bin/bash

vi /var/lib/postgresql/data/postgresql.conf
-- Adding pg_hint_plan to shared_preload_libraries
psql -h localhost -U postgres
create extension pg_hint_plan;

docker stop postgresql16.8
docker start postgresql16.8
&lt;/LI-CODE&gt;
&lt;P&gt;To connect to our docker container we use:&lt;/P&gt;
&lt;LI-CODE lang="bash"&gt;psql -h localhost -p 5414 -U postgres&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;
&lt;H4&gt;Connect and apply conservative session-level settings&lt;/H4&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;We’ll &lt;EM&gt;discourage&lt;/EM&gt; other join methods so the planner prefers &lt;STRONG&gt;Hash Join&lt;/STRONG&gt;—without needing any extension.&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;set hash_mem_multiplier=1;
set max_parallel_workers=0;
set max_parallel_workers_per_gather=0;
set enable_parallel_hash=off;
set enable_material=off;
set enable_sort=off;
set pg_hint_plan.debug_print=verbose;
set client_min_messages=notice;
set pg_hint_plan.enable_hint_table=on;&lt;/LI-CODE&gt;
&lt;UL&gt;
&lt;LI&gt;
&lt;H4&gt;Create tables and load data&lt;/H4&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;We´ll create two tables for the join, table_1, with a single row, table_h initially with 10mill rows&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;drop table table_s;
create table table_s (column_a text);
insert into table_s values ('30020');
vacuum full table_s;

drop table table_h;
create table table_h(column_a text,column_b text);

INSERT INTO table_h(column_a,column_b)
SELECT i::text, i::text
FROM generate_series(1, 10000000) AS t(i);
vacuum full table_h;&lt;/LI-CODE&gt;
&lt;UL&gt;
&lt;LI&gt;
&lt;H4&gt;Run Hash Join (high cardinality)&lt;/H4&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;We´ll run the join using column_a in both tables, that was created previously having high cardinality in table_h&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;explain (analyze,buffers,costs,verbose) SELECT /*+ HashJoin(s h) Leading((s h)) */ COUNT(*)
FROM table_s s
JOIN table_h h
  ON s.column_a= h.column_a;&lt;/LI-CODE&gt;
&lt;P&gt;You should see a Hash node with &lt;STRONG&gt;small Memory Usage (a few MB)&lt;/STRONG&gt; and &lt;STRONG&gt;Batches: 256 or similar&lt;/STRONG&gt; due to our tiny work_mem, but no ballooning. Exact numbers vary by hardware/version/stats. (EXPLAIN fields and interpretation are documented here.) &lt;A href="https://www.postgresql.org/docs/current/using-explain.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;                                                                   QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=280930.01..280930.02 rows=1 width=8) (actual time=1902.965..1902.968 rows=1 loops=1)
   Output: count(*)
   Buffers: shared read=54055, temp read=135 written=34041
   -&amp;gt;  Hash Join  (cost=279054.00..280805.01 rows=50000 width=0) (actual time=1900.539..1902.949 rows=1 loops=1)
         Hash Cond: (s.column_a = h.column_a)
         Buffers: shared read=54055, temp read=135 written=34041
         -&amp;gt;  Seq Scan on public.table_s s  (cost=0.00..1.01 rows=1 width=32) (actual time=0.021..0.022 rows=1 loops=1)
               Output: s.column_a
               Buffers: shared read=1
         -&amp;gt;  Hash  (cost=154054.00..154054.00 rows=10000000 width=32) (actual time=1896.895..1896.896 rows=10000000 loops=1)
               Output: h.column_a
               Buckets: 65536  Batches: 256  Memory Usage: 2031kB
               Buffers: shared read=54054, temp written=33785
               -&amp;gt;  Seq Scan on public.table_h h  (cost=0.00..154054.00 rows=10000000 width=32) (actual time=2.538..638.830 rows=10000000 loops=1)
                     Output: h.column_a
                     Buffers: shared read=54054
 Query Identifier: 334721522907995613
 Planning:
   Buffers: shared hit=10
 Planning Time: 0.302 ms
 JIT:
   Functions: 11
   Options: Inlining false, Optimization false, Expressions true, Deforming true
   Timing: Generation 0.441 ms, Inlining 0.000 ms, Optimization 0.236 ms, Emission 2.339 ms, Total 3.017 ms
 Execution Time: 1903.472 ms
(25 rows)&lt;/LI-CODE&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;Findings (1)&lt;/STRONG&gt; When we have the data totally distributed with high cardinality it takes only &lt;STRONG&gt;2031kB&lt;/STRONG&gt; of memory usage (work_mem), &lt;STRONG&gt;shared hit/read=54055&lt;/STRONG&gt;&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;UL&gt;
&lt;LI&gt;
&lt;H4&gt;Force&amp;nbsp;&lt;STRONG&gt;low cardinality / skew&lt;/STRONG&gt; and re‑run&lt;/H4&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;We´ll update table_h to have column_a all values to '30020', so, having only 1 distinct value for all the rows in the table&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;update table_h set column_a='30020', column_b='30020';
vacuum full table_h;&lt;/LI-CODE&gt;
&lt;P&gt;Checking execution plan:&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;                                                                   QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=279056.04..279056.05 rows=1 width=8) (actual time=3568.936..3568.938 rows=1 loops=1)
   Output: count(*)
   Buffers: shared read=54056, temp read=63480 written=63480
   -&amp;gt;  Hash Join  (cost=279055.00..279056.03 rows=1 width=0) (actual time=2650.696..3228.610 rows=10000000 loops=1)
         Hash Cond: (s.column_a = h.column_a)
         Buffers: shared read=54056, temp read=63480 written=63480
         -&amp;gt;  Seq Scan on public.table_s s  (cost=0.00..1.01 rows=1 width=32) (actual time=0.007..0.008 rows=1 loops=1)
               Output: s.column_a
               Buffers: shared read=1
         -&amp;gt;  Hash  (cost=154055.00..154055.00 rows=10000000 width=7) (actual time=1563.987..1563.989 rows=10000000 loops=1)
               Output: h.column_a
               Buckets: 131072 (originally 131072)  Batches: 512 (originally 256)  Memory Usage: 371094kB
               Buffers: shared read=54055, temp written=31738
               -&amp;gt;  Seq Scan on public.table_h h  (cost=0.00..154055.00 rows=10000000 width=7) (actual time=2.458..606.422 rows=10000000 loops=1)
                     Output: h.column_a
                     Buffers: shared read=54055
 Query Identifier: 334721522907995613
 Planning:
   Buffers: shared hit=6 read=1 dirtied=1
 Planning Time: 0.237 ms
 JIT:
   Functions: 11
   Options: Inlining false, Optimization false, Expressions true, Deforming true
   Timing: Generation 0.330 ms, Inlining 0.000 ms, Optimization 0.203 ms, Emission 2.311 ms, Total 2.844 ms
 Execution Time: 3584.439 ms
(25 rows)&lt;/LI-CODE&gt;
&lt;P&gt;Now, the Hash node typically reports &lt;STRONG&gt;hundreds of MB&lt;/STRONG&gt; of Memory Usage, with more/larger temp spills (higher Batches, more temp_blks_*). What changed? &lt;STRONG&gt;Only the distribution (cardinality)&lt;/STRONG&gt;. (Why buckets/batches behave this way is covered in the algorithm references.) &lt;A href="https://postgrespro.com/blog/pgsql/5969673" target="_blank" rel="noopener"&gt;[postgrespro.com]&lt;/A&gt;, &lt;A href="https://www.interdb.jp/pg/pgsql03/05/03.html" target="_blank" rel="noopener"&gt;[interdb.jp]&lt;/A&gt;&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;Findings (2)&lt;/STRONG&gt;&amp;nbsp;When we have the data distributed with LOW cardinality it takes &lt;STRONG&gt;371094kB &lt;/STRONG&gt;of memory usage (work_mem), shared hit/read=&lt;STRONG&gt;54056&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;So, same amount of data being handled by the query in terms of shared memory, but totally different work_mem usage pattern due to the low cardinality and the join method (Hash Join) that put most of those rows in a single bucket and that is not limited by default, so, it can cause OOM errors at any time.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;UL&gt;
&lt;LI&gt;
&lt;H4&gt;Scale up rows to observe linear growth&lt;/H4&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;We´ll add same rows in table_h repeat times so we can play with more data (low cardinality)&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;insert into table_h select * from table_h;
vacuum full table_h;&lt;/LI-CODE&gt;
&lt;P&gt;You’ll see &lt;STRONG&gt;Memory Usage and temp I/O scale with rowcount under skew&lt;/STRONG&gt;. (Beware: this can become I/O and RAM heavy—do this incrementally.) &lt;A href="https://thoughtbot.com/blog/reading-an-explain-analyze-query-plan" target="_blank" rel="noopener"&gt;[thoughtbot.com]&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table border="1" style="border-width: 1px;"&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;NumRows table_h&lt;/th&gt;&lt;th&gt;Shared read/hit&lt;/th&gt;&lt;th&gt;Dirtied&lt;/th&gt;&lt;th&gt;Written&lt;/th&gt;&lt;th&gt;Temp read/written&lt;/th&gt;&lt;th&gt;Memory Usage (work_mem)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;10M&lt;/td&gt;&lt;td&gt;54056&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;63480+63480&lt;/td&gt;&lt;td&gt;371094kB&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;20M&lt;/td&gt;&lt;td&gt;108110&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;126956+126956&lt;/td&gt;&lt;td&gt;742188kB&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;80M&lt;/td&gt;&lt;td&gt;432434 (1,64GB)&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;253908+253908&lt;/td&gt;&lt;td&gt;2968750kB (2,8GB)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 16.67%" /&gt;&lt;col style="width: 16.67%" /&gt;&lt;col style="width: 16.67%" /&gt;&lt;col style="width: 16.67%" /&gt;&lt;col style="width: 16.67%" /&gt;&lt;col style="width: 16.67%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2&gt;Observability: what you will (and won’t) see&lt;/H2&gt;
&lt;UL&gt;
&lt;LI&gt;
&lt;H4&gt;EXPLAIN is your friend&lt;/H4&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;EXPLAIN (ANALYZE, BUFFERS) exposes &lt;STRONG&gt;Memory Usage&lt;/STRONG&gt;, &lt;STRONG&gt;Buckets:&lt;/STRONG&gt;, &lt;STRONG&gt;Batches:&lt;/STRONG&gt; in the Hash node and &lt;STRONG&gt;temp block&lt;/STRONG&gt; I/O. &lt;STRONG&gt;Batches &amp;gt; 1&lt;/STRONG&gt; is a near‑certain sign of spilling. &lt;A href="https://www.postgresql.org/docs/current/using-explain.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;, &lt;A href="https://thoughtbot.com/blog/reading-an-explain-analyze-query-plan" target="_blank" rel="noopener"&gt;[thoughtbot.com]&lt;/A&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;
&lt;H4&gt;Query Store / pg_stat_statements limitations&lt;/H4&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Azure Database for PostgreSQL – Flexible Server&lt;/STRONG&gt; Query Store aggregates runtime and (optionally) wait stats over time windows and stores them in azure_sys, with views under query_store.*. It’s fantastic to find &lt;STRONG&gt;which&lt;/STRONG&gt; queries chew CPU/I/O or wait, &lt;STRONG&gt;but&lt;/STRONG&gt; it doesn’t report &lt;STRONG&gt;per‑query transient memory usage&lt;/STRONG&gt; (e.g., “how many MB did that hash table peak at?”) you can estimate reviewing the temporary blocks. &lt;A href="https://learn.microsoft.com/en-us/azure/postgresql/monitor/concepts-query-store" target="_blank" rel="noopener"&gt;[learn.microsoft.com]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;Under the hood, what you &lt;EM&gt;do&lt;/EM&gt; get—whether via Query Store or vanilla PostgreSQL pg_stat_statements—are cumulative counters like shared_blks_read, shared_blks_hit, temp_blks_read, temp_blks_written, timings, etc. Those help confirm buffer/temp activity, yet &lt;STRONG&gt;no direct hash table memory metric exists&lt;/STRONG&gt;. Combine them with EXPLAIN and server metrics to triangulate. &lt;A href="https://www.postgresql.org/docs/current/pgstatstatements.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;Tip (Azure Flexible Server)&lt;/STRONG&gt;&lt;BR /&gt;Enable Query Store in &lt;EM&gt;Server parameters&lt;/EM&gt; via pg_qs.query_capture_mode and (optionally) wait sampling via pgms_wait_sampling.query_capture_mode, then use query_store.qs_view to correlate &lt;STRONG&gt;temp block&lt;/STRONG&gt; usage and execution times across intervals. &lt;A href="https://learn.microsoft.com/en-us/azure/postgresql/monitor/concepts-query-store" target="_blank" rel="noopener"&gt;[learn.microsoft.com]&lt;/A&gt;&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;H2&gt;Typical OOM symptom in logs&lt;/H2&gt;
&lt;P&gt;In extreme skew with concurrent executions, you may encounter:&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;ERROR: out of memory&lt;/P&gt;
&lt;P&gt;DETAIL: Failed on request of size 32800 in memory context "HashBatchContext".&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;This is a classic signature of hash join memory pressure. &lt;A href="https://www.postgresql.org/message-id/B743D886-5469-4FB1-A75E-F262F399E7BA%40gmail.com" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;, &lt;A href="https://thisistheway.wiki/posts/software/postgres_memory/" target="_blank" rel="noopener"&gt;[thisistheway.wiki]&lt;/A&gt;&lt;/P&gt;
&lt;H2&gt;What to do about it (mitigations &amp;amp; best practices)&lt;/H2&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Don’t force Hash Join unless required&lt;/STRONG&gt;&lt;BR /&gt;If you used planner hints (e.g., pg_hint_plan) or GUCs (Grand Unified Configuration) to force Hash Join, remove them and let the planner re‑evaluate. (If you &lt;EM&gt;must&lt;/EM&gt; hint, be aware pg_hint_plan is a third‑party extension and not available in all environments.) &lt;A href="https://pg-hint-plan.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;[pg-hint-pl...thedocs.io]&lt;/A&gt;, &lt;A href="https://pg-hint-plan.readthedocs.io/en/latest/hint_table.html" target="_blank" rel="noopener"&gt;[pg-hint-pl...thedocs.io]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Fix skew / cardinality at the source&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI style="list-style-type: none;"&gt;
&lt;UL&gt;
&lt;LI&gt;Re‑model data to avoid low‑NDV (Number of Distinct Values in a column) joins (e.g., pre‑aggregate, filter earlier, or exclude degenerate keys).&lt;/LI&gt;
&lt;LI&gt;Ensure &lt;STRONG&gt;statistics are current&lt;/STRONG&gt; so the planner estimates are realistic. (Skew awareness is limited; poor estimates → risky sizing.) &lt;A href="https://www.postgresql.org/docs/current/using-explain.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Pick a safer join strategy when appropriate&lt;/STRONG&gt;&lt;BR /&gt;If distribution is highly skewed, &lt;STRONG&gt;Merge Join&lt;/STRONG&gt; (with supporting indexes/sort order) or &lt;STRONG&gt;Nested Loop&lt;/STRONG&gt; (for selective probes) might be more memory‑predictable. Let the planner choose, or enable alternatives by undoing GUCs that disabled them. &lt;A href="https://www.postgresql.org/docs/current/using-explain.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Bound memory consciously&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI style="list-style-type: none;"&gt;
&lt;UL&gt;
&lt;LI&gt;Keep work_mem modest for mixed/OLTP workloads; remember it’s &lt;STRONG&gt;per operation, per node, per worker&lt;/STRONG&gt;.&lt;/LI&gt;
&lt;LI&gt;Adjust hash_mem_multiplier judiciously (introduced in PG13; default now commonly 2.0) if you understand the spill trade‑offs. &lt;A href="https://postgresqlco.nf/doc/en/param/hash_mem_multiplier/" target="_blank" rel="noopener"&gt;[postgresqlco.nf]&lt;/A&gt;, &lt;A href="https://pgpedia.info/h/hash_mem_multiplier.html" target="_blank" rel="noopener"&gt;[pgpedia.info]&lt;/A&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Observe spills and tune iteratively&lt;/STRONG&gt;&lt;BR /&gt;Use EXPLAIN (ANALYZE, BUFFERS) to see Batches (spills) and Memory Usage; use Query Store/pg_stat_statements to find &lt;EM&gt;which&lt;/EM&gt; queries generate the most &lt;STRONG&gt;temp I/O&lt;/STRONG&gt;. Raise work_mem for a &lt;EM&gt;session&lt;/EM&gt; only when justified. &lt;A href="https://www.postgresql.org/docs/current/using-explain.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;, &lt;A href="https://www.postgresql.org/docs/current/pgstatstatements.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Parallelism awareness&lt;/STRONG&gt;&lt;BR /&gt;Each worker can perform its own memory‑using operations; parallel hash join has distinct behavior. If you aren’t sure, temporarily disable parallelism to simplify analysis, then re‑enable once you understand the footprint. &lt;A href="https://www.postgresql.org/docs/current/using-explain.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;Validating on Azure Database for PostgreSQL – Flexible Server&lt;/H2&gt;
&lt;P&gt;The behavior is &lt;STRONG&gt;not Azure‑specific&lt;/STRONG&gt;, but you can reproduce the same sequence on Flexible Server (e.g., General Purpose). A few notes:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Confirm/adjust work_mem, hash_mem_multiplier, enable_* planner toggles as session settings. (Azure exposes standard PostgreSQL parameters.) &lt;A href="https://learn.microsoft.com/en-us/azure/postgresql/server-parameters/param-resource-usage-memory" target="_blank" rel="noopener"&gt;[learn.microsoft.com]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;Use &lt;STRONG&gt;Query Store&lt;/STRONG&gt; to confirm stable &lt;STRONG&gt;shared/temporary block&lt;/STRONG&gt; patterns across executions, then use EXPLAIN (ANALYZE, BUFFERS) per query to spot &lt;STRONG&gt;hash table memory&lt;/STRONG&gt; footprints. &lt;A href="https://learn.microsoft.com/en-us/azure/postgresql/monitor/concepts-query-store" target="_blank" rel="noopener"&gt;[learn.microsoft.com]&lt;/A&gt;, &lt;A href="https://www.postgresql.org/docs/current/using-explain.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;
&lt;H4&gt;Changing some default parameters&lt;/H4&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;We´ll repeat previous steps in Azure Database for PostgreSQL Flexible server:&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;set hash_mem_multiplier=1;
set max_parallel_workers=0;
set max_parallel_workers_per_gather=0;
set enable_parallel_hash=off;
set enable_material=off;
set enable_sort=off;
set pg_hint_plan.debug_print=verbose;
set client_min_messages=notice;
set pg_hint_plan.enable_hint_table=on;&lt;/LI-CODE&gt;
&lt;UL&gt;
&lt;LI&gt;
&lt;H4&gt;Creating and populating tables&lt;/H4&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;LI-CODE lang="sql"&gt;drop table table_s;
create table table_s (column_a text);
insert into table_s values ('30020');
vacuum full table_s;
 
drop table table_h;
create table table_h(column_a text,column_b text);
 
INSERT INTO table_h(column_a,column_b)
SELECT i::text, i::text
FROM generate_series(1, 10000000) AS t(i);
vacuum full table_h;
vacuum full table_s;&lt;/LI-CODE&gt;
&lt;UL&gt;
&lt;LI&gt;
&lt;H4&gt;Query &amp;amp; Execution plan&lt;/H4&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;LI-CODE lang="sql"&gt;explain (analyze,buffers,costs,verbose) SELECT /*+ HashJoin(s h) Leading((s h)) */ COUNT(*)
FROM table_s s
JOIN table_h h
  ON s.column_a= h.column_a;&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;                                                                   QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=279052.88..279052.89 rows=1 width=8) (actual time=3171.186..3171.191 rows=1 loops=1)
   Output: count(*)
   Buffers: shared hit=33 read=54023, temp read=135 written=34042
   I/O Timings: shared read=184.869, temp read=0.278 write=333.970
   -&amp;gt;  Hash Join  (cost=279051.84..279052.88 rows=1 width=0) (actual time=3147.288..3171.182 rows=1 loops=1)
         Hash Cond: (s.column_a = h.column_a)
         Buffers: shared hit=33 read=54023, temp read=135 written=34042
         I/O Timings: shared read=184.869, temp read=0.278 write=333.970
         -&amp;gt;  Seq Scan on public.table_s s  (cost=0.00..1.01 rows=1 width=32) (actual time=0.315..0.316 rows=1 loops=1)
               Output: s.column_a
               Buffers: shared read=1
               I/O Timings: shared read=0.018
         -&amp;gt;  Hash  (cost=154053.04..154053.04 rows=9999904 width=7) (actual time=3109.278..3109.279 rows=10000000 loops=1)
               Output: h.column_a
               Buckets: 131072  Batches: 256  Memory Usage: 2551kB
               Buffers: shared hit=32 read=54022, temp written=33786
               I/O Timings: shared read=184.851, temp write=332.059
               -&amp;gt;  Seq Scan on public.table_h h  (cost=0.00..154053.04 rows=9999904 width=7) (actual time=0.019..1258.472 rows=10000000 loops=1)
                     Output: h.column_a
                     Buffers: shared hit=32 read=54022
                     I/O Timings: shared read=184.851
 Query Identifier: 5636209387670245929
 Planning:
   Buffers: shared hit=37
 Planning Time: 0.575 ms
 Execution Time: 3171.375 ms
(26 rows)&lt;/LI-CODE&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;&lt;STRONG&gt;Findings (3)&lt;/STRONG&gt;&amp;nbsp;In Azure Database for PostgreSQL Flexible server, when we have the data totally distributed with high cardinality it takes only &lt;STRONG&gt;2551kB &lt;/STRONG&gt;of memory usage (work_mem), shared hit/read=54056&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;
&lt;H4&gt;Skew it to LOW cardinality&lt;/H4&gt;
&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;As we did previously, we change column_a to having only one different value in all rows in table_h&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;update table_h set column_a='30020', column_b='30020';
vacuum full table_h;&lt;/LI-CODE&gt;
&lt;P&gt;In this case we force the join method with pg_hint_plan:&amp;nbsp;&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;explain (analyze,buffers,costs,verbose) SELECT /*+ HashJoin(s h) Leading((s h)) */ COUNT(*)
FROM table_s s
JOIN table_h h
  ON s.column_a= h.column_a;

                                                                   QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=279056.04..279056.05 rows=1 width=8) (actual time=4397.556..4397.560 rows=1 loops=1)
   Output: count(*)
   Buffers: shared hit=2 read=54055, temp read=63480 written=63480
   I/O Timings: shared read=89.396, temp read=90.377 write=300.290
   -&amp;gt;  Hash Join  (cost=279055.00..279056.03 rows=1 width=0) (actual time=3271.145..3987.154 rows=10000000 loops=1)
         Hash Cond: (s.column_a = h.column_a)
         Buffers: shared hit=2 read=54055, temp read=63480 written=63480
         I/O Timings: shared read=89.396, temp read=90.377 write=300.290
         -&amp;gt;  Seq Scan on public.table_s s  (cost=0.00..1.01 rows=1 width=32) (actual time=0.006..0.008 rows=1 loops=1)
               Output: s.column_a
               Buffers: shared hit=1
         -&amp;gt;  Hash  (cost=154055.00..154055.00 rows=10000000 width=7) (actual time=1958.729..1958.731 rows=10000000 loops=1)
               Output: h.column_a
               Buckets: 262144 (originally 262144)  Batches: 256 (originally 128)  Memory Usage: 371094kB
               Buffers: shared read=54055, temp written=31738
               I/O Timings: shared read=89.396, temp write=149.076
               -&amp;gt;  Seq Scan on public.table_h h  (cost=0.00..154055.00 rows=10000000 width=7) (actual time=0.159..789.449 rows=10000000 loops=1)
                     Output: h.column_a
                     Buffers: shared read=54055
                     I/O Timings: shared read=89.396
 Query Identifier: 8893575855188549861
 Planning:
   Buffers: shared hit=5
 Planning Time: 0.157 ms
 Execution Time: 4414.268 ms
(25 rows)&lt;/LI-CODE&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;DIV class="styles_lia-table-wrapper__h6Xo9 styles_table-responsive__MW0lN"&gt;&lt;table border="1" style="border-width: 1px;"&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;NumRows table_h&lt;/th&gt;&lt;th&gt;Shared read/hit&lt;/th&gt;&lt;th&gt;Dirtied&lt;/th&gt;&lt;th&gt;Written&lt;/th&gt;&lt;th&gt;Temp read/written&lt;/th&gt;&lt;th&gt;Memory Usage (work_mem)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;10M&lt;/td&gt;&lt;td&gt;54056&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;63480+63480&lt;/td&gt;&lt;td&gt;371094kB&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;20M&lt;/td&gt;&lt;td&gt;108110&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&lt;td&gt;126956+126956&lt;/td&gt;&lt;td&gt;742188kB&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;80M&lt;/td&gt;&lt;td&gt;432434&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;253908+253908&lt;/td&gt;&lt;td&gt;2968750kB&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;colgroup&gt;&lt;col style="width: 16.67%" /&gt;&lt;col style="width: 16.67%" /&gt;&lt;col style="width: 16.67%" /&gt;&lt;col style="width: 16.67%" /&gt;&lt;col style="width: 16.67%" /&gt;&lt;col style="width: 16.67%" /&gt;&lt;/colgroup&gt;&lt;/table&gt;&lt;/DIV&gt;
&lt;P&gt;We observe the same numbers compared with our docker installation.&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P&gt;See the extension docs for installation/usage and the&amp;nbsp;&lt;STRONG&gt;hint table&lt;/STRONG&gt; for cases where you want to force a specific join method.&amp;nbsp;&lt;A href="https://pg-hint-plan.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;[pg-hint-pl...thedocs.io]&lt;/A&gt;, &lt;A href="https://pg-hint-plan.readthedocs.io/en/latest/hint_table.html" target="_blank" rel="noopener"&gt;[pg-hint-pl...thedocs.io]&lt;/A&gt;&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;H2&gt;FAQ&lt;/H2&gt;
&lt;P&gt;&lt;STRONG&gt;Q: I set work_mem = 4MB. Why did my Hash Join report ~371MB Memory Usage?&lt;/STRONG&gt;&lt;BR /&gt;A: Hash joins can use up to hash_mem_multiplier × work_mem &lt;EM&gt;per hash table&lt;/EM&gt;, and skew can cause large per‑bucket chains. Multiple nodes/workers multiply usage. work_mem is not a global hard cap. &lt;A href="https://postgresqlco.nf/doc/en/param/hash_mem_multiplier/" target="_blank" rel="noopener"&gt;[postgresqlco.nf]&lt;/A&gt;, &lt;A href="https://pgpedia.info/h/hash_mem_multiplier.html" target="_blank" rel="noopener"&gt;[pgpedia.info]&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Q: How do I know if a Hash Join spilled to disk?&lt;/STRONG&gt;&lt;BR /&gt;A: In EXPLAIN (ANALYZE), Hash node shows Batches: N. &lt;STRONG&gt;N &amp;gt; 1&lt;/STRONG&gt; indicates partitioning and temp I/O; you’ll also see temp_blks_read/written in buffers and Temp I/O timings. &lt;A href="https://www.postgresql.org/docs/current/using-explain.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;, &lt;A href="https://thoughtbot.com/blog/reading-an-explain-analyze-query-plan" target="_blank" rel="noopener"&gt;[thoughtbot.com]&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Q: Can Query Store tell me per‑query memory consumption?&lt;/STRONG&gt;&lt;BR /&gt;A: Not directly. It gives time‑sliced runtime and wait stats (plus temp/shared block counters via underlying stats), but no “peak MB used by this hash table” metric. Use EXPLAIN and server metrics. &lt;A href="https://learn.microsoft.com/en-us/azure/postgresql/monitor/concepts-query-store" target="_blank" rel="noopener"&gt;[learn.microsoft.com]&lt;/A&gt;, &lt;A href="https://www.postgresql.org/docs/current/pgstatstatements.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Q: I hit “Failed on request … in HashBatchContext.” What’s that?&lt;/STRONG&gt;&lt;BR /&gt;A: That’s an OOM raised by the executor while allocating memory. Reduce skew, avoid forced hash joins, or review per‑query memory and concurrency.&amp;nbsp;&lt;A href="https://www.postgresql.org/message-id/B743D886-5469-4FB1-A75E-F262F399E7BA%40gmail.com" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;&lt;/P&gt;
&lt;H2&gt;Further reading&lt;/H2&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;STRONG&gt;Server parameters &amp;amp; memory&lt;/STRONG&gt; (official docs): guidance on work_mem, shared_buffers, parallelism. &lt;A href="https://www.postgresql.org/docs/current/runtime-config-resource.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Hash joins under the hood&lt;/STRONG&gt;: deep dives into buckets, batches, and memory footprints. &lt;A href="https://postgrespro.com/blog/pgsql/5969673" target="_blank" rel="noopener"&gt;[postgrespro.com]&lt;/A&gt;, &lt;A href="https://www.pgcon.org/2017/schedule/attachments/455_pgcon-2017-hash-joins.pdf" target="_blank" rel="noopener"&gt;[pgcon.org]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;hash_mem_multiplier&lt;/STRONG&gt;: history and defaults by version. &lt;A href="https://pgpedia.info/h/hash_mem_multiplier.html" target="_blank" rel="noopener"&gt;[pgpedia.info]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;EXPLAIN primer&lt;/STRONG&gt;: how to read Hash node details, Batches, Memory Usage. &lt;A href="https://www.postgresql.org/docs/current/using-explain.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;, &lt;A href="https://thoughtbot.com/blog/reading-an-explain-analyze-query-plan" target="_blank" rel="noopener"&gt;[thoughtbot.com]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Query Store (Azure Flexible)&lt;/STRONG&gt;: enable, query, and interpret. &lt;A href="https://learn.microsoft.com/en-us/azure/postgresql/monitor/concepts-query-store" target="_blank" rel="noopener"&gt;[learn.microsoft.com]&lt;/A&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;Ready‑to‑use mitigation checklist (DBA quick wins)&lt;/H2&gt;
&lt;UL&gt;
&lt;LI&gt;Remove joins hints/GUC overrides that force Hash Join; re‑plan. &lt;A href="https://pg-hint-plan.readthedocs.io/en/latest/" target="_blank" rel="noopener"&gt;[pg-hint-pl...thedocs.io]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;Refresh stats; confirm realistic rowcount/NDV estimates. &lt;A href="https://www.postgresql.org/docs/current/using-explain.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;Consider alternate join strategies (Merge/Index‑Nested‑Loop) when skew is high. &lt;A href="https://www.postgresql.org/docs/current/using-explain.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;Keep work_mem conservative for OLTP; consider session‑scoped bumps only for specific analytic queries. &lt;A href="https://www.postgresql.org/docs/current/runtime-config-resource.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;Tune hash_mem_multiplier carefully only after understanding spill patterns. &lt;A href="https://postgresqlco.nf/doc/en/param/hash_mem_multiplier/" target="_blank" rel="noopener"&gt;[postgresqlco.nf]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;Use EXPLAIN (ANALYZE, BUFFERS) to verify Batches and Memory Usage. &lt;A href="https://www.postgresql.org/docs/current/using-explain.html" target="_blank" rel="noopener"&gt;[postgresql.org]&lt;/A&gt;&lt;/LI&gt;
&lt;LI&gt;Use Query Store/pg_stat_statements to find heavy temp/shared I/O offenders over time.&lt;/LI&gt;
&lt;/UL&gt;</description>
      <pubDate>Mon, 09 Mar 2026 13:38:54 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/understanding-hash-join-memory-usage-and-oom-risks-in-postgresql/ba-p/4500308</guid>
      <dc:creator>FranciscoPardillo</dc:creator>
      <dc:date>2026-03-09T13:38:54Z</dc:date>
    </item>
    <item>
      <title>Dashboards with Grafana - Now in Azure Portal for PostgreSQL</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/dashboards-with-grafana-now-in-azure-portal-for-postgresql/ba-p/4497607</link>
      <description>&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;Monitoring Azure Database for PostgreSQL just got significantly simpler. With the new&lt;STRONG&gt; &lt;/STRONG&gt;&lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/azure-monitor/visualize/visualize-use-grafana-dashboards" target="_blank" rel="noopener"&gt;Azure Monitor Dashboards with Grafana&lt;/A&gt;, you can visualize key metrics and logs directly inside Azure Portal - no Grafana servers to deploy, no configuration to manage, and no additional cost&lt;/P&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;In this post, we’ll walk through how these built-in Grafana dashboards help you troubleshoot faster, understand database behavior at a glance, and decide when you might still want Azure Managed Grafana for advanced scenarios.&lt;/P&gt;
&lt;H1&gt;Native Grafana Dashboards — No Setup, No Hosting, No Cost&lt;/H1&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;We are thrilled to announce that &lt;A class="lia-external-url" href="https://azure.microsoft.com/products/postgresql" target="_blank" rel="noopener"&gt;Azure Database for PostgreSQL&lt;/A&gt; users can now access &lt;STRONG&gt;prebuilt Grafana dashboards directly within the Azure portal&lt;/STRONG&gt; - with &lt;STRONG&gt;no additional cost or configuration required&lt;/STRONG&gt;. This integration eliminates the complexity of deploying and administering self-hosted or managed Grafana instances. Grafana’s powerful visualization capabilities are now embedded directly in the Azure experience&lt;BR /&gt;&lt;BR /&gt;From the moment you open the Azure Portal you have immediate access to dashboards for PostgreSQL. Simply navigate to Azure Database for PostgreSQL server in the Azure Portal and select “&lt;STRONG&gt;Dashboards with Grafana&lt;/STRONG&gt;” and choose &lt;EM&gt;Featured dashboards&lt;/EM&gt;. Within seconds, you have a rich, real-time view of your database server’s health - no custom queries or manual wiring required.&lt;/P&gt;
&lt;img /&gt;
&lt;P class="lia-align-center"&gt;&lt;SPAN class="lia-text-color-19"&gt;&lt;EM&gt;Figure 1: Azure Portal showing the “Dashboards with Grafana” blade , featuring the prebuilt monitoring dashboard tile.&lt;/EM&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H3&gt;Comprehensive PostgreSQL Metrics at a Glance&lt;/H3&gt;
&lt;img /&gt;
&lt;P class="lia-align-center" style="font-size: 18px; line-height: 1.7;"&gt;&lt;SPAN class="lia-text-color-19"&gt;&lt;EM&gt;Figure 2: Azure PostgreSQL Grafana dashboard showing resource utilization, performance metrics, and server configuration.&lt;/EM&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;As shown above, the new Grafana dashboard provides &lt;STRONG&gt;at-a-glance visibility&lt;/STRONG&gt; into all the key metrics that matter for Azure Database for PostgreSQL. These dashboards are purpose-built to surface the health and performance of your database server, so you can immediately spot trends or issues.&lt;/P&gt;
&lt;H3&gt;Quick Configuration Snapshot&lt;/H3&gt;
&lt;img /&gt;
&lt;P class="lia-align-center"&gt;&lt;SPAN class="lia-text-color-19"&gt;&lt;EM&gt;Figure 3: PostgreSQL server details, showing version, region, compute size, availability, and resource usage gauges.&lt;/EM&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;Every monitoring session starts with instant answers to critical questions:&lt;/P&gt;
&lt;UL class="lia-align-justify"&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Is the server up?&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Is High Availability configured?&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;How much storage is available?&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;The summary panel provides:&lt;/P&gt;
&lt;UL class="lia-align-justify"&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Instance status (Up/Down)&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;High Availability and replica configuration&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Azure region, PostgreSQL version, and SKU&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Live resource usage (CPU, memory, storage)&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;No extra clicks. No custom queries. Just clarity.&lt;/P&gt;
&lt;H2&gt;Metrics Coverage&lt;/H2&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;The prebuilt dashboards visualize telemetry emitted by Azure Database for PostgreSQL, including:&lt;/P&gt;
&lt;UL class="lia-align-justify"&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;&lt;STRONG&gt;Server availability&lt;/STRONG&gt; and status&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;&lt;STRONG&gt;Active connections&lt;/STRONG&gt; and connection failures&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;&lt;STRONG&gt;CPU&lt;/STRONG&gt; and &lt;STRONG&gt;memory&lt;/STRONG&gt; utilization&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;&lt;STRONG&gt;Storage usage&lt;/STRONG&gt; and WAL consumption&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;&lt;STRONG&gt;Disk I/O&lt;/STRONG&gt; (IOPS and throughput)&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;&lt;STRONG&gt;Network&lt;/STRONG&gt; ingress and egress&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;&lt;STRONG&gt;Transaction&lt;/STRONG&gt; rates, commits, and rollbacks&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;These metrics are collected via Azure Monitor platform metrics and refreshed at near-real-time intervals (depending on metric type). For a complete list, see the&amp;nbsp;&lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/postgresql/monitor/concepts-monitoring" target="_blank" rel="noopener"&gt;Azure Database for PostgreSQL monitoring&lt;/A&gt; documentation.&lt;/P&gt;
&lt;H1&gt;Metrics and Logs—Together&lt;/H1&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;Ever struggled to trace a spike in CPU to the actual query behind it? With PostgreSQL logs and metrics visualized side-by-side, you can now correlate the &lt;STRONG&gt;exact timestamp&lt;/STRONG&gt; of a CPU surge with detailed &lt;STRONG&gt;query logs&lt;/STRONG&gt; in seconds.&lt;/P&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;P class="lia-align-center"&gt;&lt;SPAN class="lia-text-color-19"&gt;&lt;EM&gt;Figure 4:&amp;nbsp;&lt;/EM&gt;&lt;EM data-start="1045" data-end="1202"&gt;CPU usage metrics co-relation with PostgreSQL log entries in Azure Monitor, highlighting slow query detection and log integration.&lt;/EM&gt;&lt;/SPAN&gt;&lt;/P&gt;
&lt;BLOCKQUOTE&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;&lt;SPAN class="lia-text-color-13"&gt;&lt;STRONG&gt;💡Note&lt;/STRONG&gt;: To view logs in Grafana, make sure diagnostic settings are enabled to send PostgreSQL logs to Azure Monitor Logs. You can configure this in the Azure Portal under your PostgreSQL resource &amp;gt; Monitoring &amp;gt; Diagnostic settings.&lt;/SPAN&gt;&amp;nbsp;&lt;A href="https://learn.microsoft.com/en-us/azure/postgresql/monitor/how-to-configure-and-access-logs" target="_blank" rel="noopener"&gt;Learn how&lt;/A&gt;.&lt;/P&gt;
&lt;/BLOCKQUOTE&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;In the example above, high CPU usage (&lt;STRONG&gt;73.2%&lt;/STRONG&gt;) aligns precisely with poor-running queries against a large salesorderdetail_big table. This helps engineers instantly validate and pinpoint slow queries without jumping between tools.&lt;/P&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;The unified Metric + Logs view, you can:&lt;/P&gt;
&lt;UL class="lia-align-justify"&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Plot query errors over time&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Correlate failed logins with resource spikes&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Investigate locking or memory pressure using timestamps&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;Grafana &lt;STRONG&gt;Explore mode&lt;/STRONG&gt; is also available for deep-dive troubleshooting without altering dashboards.&lt;/P&gt;
&lt;H1&gt;First-Class Azure Integration&lt;/H1&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;This is not just embedded Grafana - it is &lt;STRONG&gt;first-class&lt;/STRONG&gt; Azure-native:&lt;/P&gt;
&lt;UL class="lia-align-justify"&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Dashboards are &lt;STRONG&gt;Azure resources&lt;/STRONG&gt;, scoped to subscriptions and resource groups&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Access is controlled using &lt;STRONG&gt;Azure RBAC&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Dashboards can be exported and deployed via &lt;STRONG&gt;ARM&lt;/STRONG&gt; templates&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Easy sharing and migration across environments&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;You get the flexibility of open-source Grafana with Azure’s enterprise-grade governance.&lt;/P&gt;
&lt;H1&gt;Getting Started&lt;/H1&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;To use the pre-built dashboard&lt;/P&gt;
&lt;OL class="lia-align-justify"&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Open the Azure portal&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Navigate to &lt;STRONG&gt;Azure Database for PostgreSQL&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Select &lt;STRONG&gt;Dashboards with Grafana&lt;/STRONG&gt;&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Open the PostgreSQL featured dashboard&lt;/LI&gt;
&lt;/OL&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;To customize a dashboard:&lt;/P&gt;
&lt;OL class="lia-align-justify"&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Open the prebuilt PostgreSQL dashboard&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Select &lt;STRONG&gt;Save As&lt;/STRONG&gt; to create a copy&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Modify panels or add new visualizations&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Connect additional data sources (metrics or logs)&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Save and share with your team&lt;/LI&gt;
&lt;/OL&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;For advanced customization, refer to the &lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/azure-monitor/visualize/visualize-use-grafana-dashboards" target="_blank" rel="noopener"&gt;Azure Monitor + Grafana Learn documentation&lt;/A&gt;.&lt;/P&gt;
&lt;H1&gt;When to Use Azure Managed Grafana?&lt;/H1&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;Dashboards with Grafana in the Azure portal cover most common PostgreSQL monitoring scenarios. &lt;A class="lia-external-url" href="https://azure.microsoft.com/products/managed-grafana" target="_blank" rel="noopener"&gt;Azure Managed Grafana&lt;/A&gt; is still the right choice when you need:&lt;/P&gt;
&lt;UL class="lia-align-justify"&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Extended plugin support (community and OSS plugins)&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Advanced authentication and provisioning APIs&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Fine-grained, multi-tenant access control&lt;/LI&gt;
&lt;LI style="font-size: 18px; line-height: 1.7;"&gt;Multi-cloud or hybrid data source connectivity&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;See the &lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/azure-monitor/visualize/visualize-grafana-overview#solution-comparison" target="_blank" rel="noopener"&gt;detailed comparison&lt;/A&gt; to choose the right option.&lt;/P&gt;
&lt;H1&gt;Learn More&lt;/H1&gt;
&lt;UL&gt;
&lt;LI class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;&lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/postgresql/monitor/concepts-monitoring" target="_blank" rel="noopener"&gt;Azure PostgreSQL Monitoring Overview&lt;/A&gt;&lt;/LI&gt;
&lt;LI class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;&lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/azure-monitor/visualize/visualize-grafana-overview" target="_blank" rel="noopener"&gt;Visualize Azure Monitor Data with Grafana&lt;/A&gt;&lt;/LI&gt;
&lt;LI class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;&lt;A class="lia-internal-link lia-internal-url lia-internal-url-content-type-blog" href="https://techcommunity.microsoft.com/blog/azureobservabilityblog/announcing-general-availability-azure-monitor-dashboards-with-grafana/4468972" target="_blank" rel="noopener" data-lia-auto-title="GA Blog: Azure Monitor Dashboard with Grafana" data-lia-auto-title-active="0"&gt;GA Blog: Azure Monitor Dashboard with Grafana&lt;/A&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;P class="lia-align-justify" style="font-size: 18px; line-height: 1.7;"&gt;Start visualizing your &lt;A class="lia-external-url" href="https://learn.microsoft.com/azure/postgresql/" target="_blank" rel="noopener"&gt;Azure PostgreSQL&lt;/A&gt; data instantly—right where you already work.&lt;/P&gt;</description>
      <pubDate>Thu, 26 Feb 2026 19:19:45 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/dashboards-with-grafana-now-in-azure-portal-for-postgresql/ba-p/4497607</guid>
      <dc:creator>varun-dhawan</dc:creator>
      <dc:date>2026-02-26T19:19:45Z</dc:date>
    </item>
    <item>
      <title>Nasdaq builds thoughtfully designed AI for board governance with PostgreSQL on Azure</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/nasdaq-builds-thoughtfully-designed-ai-for-board-governance-with/ba-p/4493078</link>
      <description>&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;EM&gt;Authored by:&lt;STRONG&gt; &lt;/STRONG&gt;Charles Federssen, Partner Director of Product Management for PostgreSQL at Microsoft and Mohsin Shafqat, Senior Manager, Software Engineering at Nasdaq&lt;/EM&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;When people think of Nasdaq, they usually think of markets, trading floors, and financial data moving at extraordinary speed. But behind the scenes, Nasdaq also plays an equally critical role in how boards of directors govern, deliberate, and make decisions.&lt;/P&gt;
&lt;P&gt;Nasdaq Boardvantage® is the company’s governance platform, used by more than 4,400 organizations worldwide—including nearly half of the Fortune 100. It’s where directors review board books, collaborate in an environment designed with robust security, and prepare for meetings that often involve some of the most sensitive information a company has.&lt;/P&gt;
&lt;P&gt;In recent years, Nasdaq set out to modernize Nasdaq Boardvantage with AI, without compromising security and reliability. That journey was featured in a Microsoft Ignite session, “&lt;A class="lia-external-url" href="https://www.youtube.com/watch?v=BkOcPQntsk4" target="_blank" rel="noopener"&gt;Nasdaq Boardvantage: AI-Driven Governance on PostgreSQL and Foundry&lt;/A&gt;.” It offers a practical look at how Azure Database for PostgreSQL can support AI-driven applications where precision, isolation, and data control are non-negotiable.&lt;/P&gt;
&lt;H2&gt;Introducing AI where trust is everything&lt;/H2&gt;
&lt;P&gt;Board governance isn’t a typical productivity workload. Board packets can run 400 to 600 pages, meeting minutes are legal records, and any AI-generated insight must be confined to a customer’s own data.&lt;/P&gt;
&lt;P&gt;“Our customers trust us with some of their most strategic, sensitive data,” said Mohsin Shafqat, Senior Manager of Software Development at Nasdaq. That trust meant tackling several core challenges upfront, including:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;How do you minimize AI hallucinations in a governance context?&lt;/LI&gt;
&lt;LI&gt;How do you guarantee tenant isolation at scale?&lt;/LI&gt;
&lt;LI&gt;How do you keep data regional across a global customer base?&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2&gt;A cloud foundation built for governance&lt;/H2&gt;
&lt;P&gt;Before adding intelligence, Nasdaq decided to re-architect Nasdaq Boardvantage on Microsoft Azure, using Azure Kubernetes Service (AKS) to run containerized, multi-tenant workloads with strong isolation boundaries. Microsoft Foundry provides the managed foundation for deploying, governing, and operating AI models across this architecture, adding consistency, security, and control as intelligence is introduced.&lt;/P&gt;
&lt;P&gt;At the data layer, Azure Database for PostgreSQL and Azure Database for MySQL became the backbone for governance data. PostgreSQL, in particular, plays a central role in managing structured governance information alongside vector embeddings that support AI-driven features. Together, these services give Nasdaq the performance, security, and operational control required for a highly regulated, multi-tenant environment, while still moving quickly.&lt;/P&gt;
&lt;P&gt;Key architectural choices included:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Tenant isolation by design, with separate databases and storage&lt;/LI&gt;
&lt;LI&gt;Regional deployments to align with data residency requirements&lt;/LI&gt;
&lt;LI&gt;High availability and managed operations, so teams could focus on product innovation instead of infrastructure maintenance&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;PostgreSQL and pgvector: Powering context-aware AI&lt;/H2&gt;
&lt;P&gt;With that foundation in place, Nasdaq was ready to carefully introduce AI. One of the first AI capabilities was intelligent document summarization. Board materials that once took hours to review could now be condensed into concise, contextually accurate summaries.&lt;/P&gt;
&lt;P&gt;Under the hood, this required more than just calling an LLM. Nasdaq uses pgvector, natively supported in Azure Database for PostgreSQL, to store and query embeddings generated from board documents. This allows the platform to perform hybrid searches that combine traditional SQL queries with vector similarity to retrieve the most relevant context before sending anything to a language model.&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;Instead of treating AI as a black box, the team built a pipeline where:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Documents are processed with Azure Document Intelligence to preserve structure and meaning&lt;/LI&gt;
&lt;LI&gt;Content is chunked and embedded&lt;/LI&gt;
&lt;LI&gt;Embeddings are stored in PostgreSQL with pgvector&lt;/LI&gt;
&lt;LI&gt;Vector similarity searches retrieve precise context for each AI task&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;Because this runs inside PostgreSQL, the same database benefits from Azure’s built-in high availability, security controls, and operational tooling – delivering tangible results, including a 25% reduction in overall board preparation time and internal testing shows 91–97% accuracy for AI-generated summaries and meeting minutes.&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2&gt;From summaries to an AI Board Assistant&lt;/H2&gt;
&lt;P&gt;With summarization working in production, Nasdaq expanded further. The team is now building an AI-powered Board Assistant that will help directors prepare for upcoming meetings by surfacing trends, risks, and insights from prior discussions.&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;This introduces a new level of scale. Years of board data across thousands of customers translate into millions of embeddings. PostgreSQL continues to anchor this architecture, storing vectors for semantic retrieval while MySQL supports complementary non-vector workloads. Across Nasdaq Boardvantage, users are advised to always review AI outputs, and no customer data is shared or used to train external models. “We designed AI for governance, not the other way around,” Shafqat said.&lt;/P&gt;
&lt;P&gt;More importantly, customers trust the system because security, isolation, and data control were engineered in from day one.&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;H2&gt;Looking ahead&lt;/H2&gt;
&lt;P&gt;Nasdaq’s work shows how Azure Database for PostgreSQL can support AI workloads that demand both intelligence and integrity. With PostgreSQL at the core, Nasdaq has built a governance platform that scales globally, respects regulatory boundaries, and introduces AI in a way that feels dependable and not experimental.&lt;/P&gt;
&lt;P&gt;What started as a modernization of Nasdaq Boardvantage is now influencing how Nasdaq approaches AI across the enterprise.&lt;/P&gt;
&lt;P&gt;To dive deeper into the architecture and hear directly from the engineers behind it, watch the Ignite session and check out these resources:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;A class="lia-external-url" href="https://www.youtube.com/watch?v=BkOcPQntsk4" target="_blank" rel="noopener"&gt;Watch the Ignite breakout session&lt;/A&gt; for a technical walkthrough of how Nasdaq Boardvantage is built, including PostgreSQL on Azure, pgvector, and Microsoft Foundry in production.&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://www.microsoft.com/en/customers/story/25682-nasdaq-azure" target="_blank" rel="noopener"&gt;Read the case study&lt;/A&gt; to see how Nasdaq introduced AI into board governance and what changed for directors, administrators, and decision-making.&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://ignite.microsoft.com/en-US/sessions/STUDIO34?source=sessions" target="_blank" rel="noopener"&gt;Watch the Ignite broadcast&lt;/A&gt; for a candid discussion on Azure Database for PostgreSQL, Azure HorizonDB, and what it takes to scale AI-driven governance.&lt;/LI&gt;
&lt;/UL&gt;</description>
      <pubDate>Wed, 25 Feb 2026 05:34:02 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/nasdaq-builds-thoughtfully-designed-ai-for-board-governance-with/ba-p/4493078</guid>
      <dc:creator>charlesfeddersenMS</dc:creator>
      <dc:date>2026-02-25T05:34:02Z</dc:date>
    </item>
    <item>
      <title>Microsoft at PGConf India 2026</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/microsoft-at-pgconf-india-2026/ba-p/4496182</link>
      <description>&lt;P&gt;I’m genuinely excited about &lt;A href="https://www.pgconf.in/conferences/pgconfin2026" target="_blank" rel="noopener"&gt;PGConf India 2026&lt;/A&gt;. Over the past few editions, the conference has continued to grow year over year—both in size and in impact—and it has firmly established itself as one of the key events on the global PostgreSQL calendar. That momentum was very evident again in the depth, breadth, and overall quality of the program for PGConf India 2026. Microsoft is proud to be a &lt;A class="lia-external-url" href="https://live.pgconf.in/sponsors" target="_blank" rel="noopener"&gt;diamond sponsor&lt;/A&gt; for the conference again this year.&amp;nbsp;&lt;/P&gt;
&lt;P&gt;At Microsoft, we &lt;A href="https://techcommunity.microsoft.com/blog/adforpostgresql/whats-new-with-postgres-at-microsoft-2025-edition/4410710#community-4410710-postgres-core" target="_blank" rel="noopener"&gt;continue our contributions&lt;/A&gt; to the upstream PostgreSQL open-source project—as well as to serve our customers with our Postgres managed service offerings, both &lt;A href="https://learn.microsoft.com/azure/postgresql/overview" target="_blank" rel="noopener"&gt;Azure Database for PostgreSQL&lt;/A&gt; and our newest Postgres offering, &lt;A href="https://techcommunity.microsoft.com/blog/adforpostgresql/announcing-azure-horizondb/4469710" target="_blank" rel="noopener"&gt;Azure HorizonDB&lt;/A&gt;&amp;nbsp;. On the open-source front, Microsoft had 540 commits in PG18, including major features like Asynchronous IO. We’re also excited to grow our Postgres open-source contributors team, and so happy to welcome Noah Misch to our team. Noah is a Postgres committer who has deep expertise in PostgreSQL security and is focused on correctness and reliability in PostgreSQL’s core.&lt;/P&gt;
&lt;H2&gt;Microsoft at PGConf India 2026: Highlights from Our Speakers&lt;/H2&gt;
&lt;P&gt;PGConf India has several tracks, all of which have some great talks I am looking forward to. First, the plug. 😊 Microsoft has some amazing talks this year, and we have 8 different talks spread across all the tracks.&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;A href="https://live.pgconf.in/schedule/175" target="_blank" rel="noopener"&gt;Postgres on Azure : Scaling with Azure HorizonDB, AI, and Developer Workflows&lt;/A&gt;, by Aditya Duvuri &amp;amp; Divya Bhargov&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://live.pgconf.in/schedule/143" target="_blank" rel="noopener"&gt;Resizing shared buffer pool in a running PostgreSQL server: important, yet impossible&lt;/A&gt;,&amp;nbsp; by Ashutosh Bapat&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://live.pgconf.in/schedule/158" target="_blank" rel="noopener"&gt;Ten Postgres Hacker Journeys—and what they teach us&lt;/A&gt;, by Claire Giordano&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://live.pgconf.in/schedule/136" target="_blank" rel="noopener"&gt;How Postgres can leverage disk bandwidth for better TPS&lt;/A&gt;, by Nikhil Chawla&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://live.pgconf.in/schedule/156" target="_blank" rel="noopener"&gt;AWSM FSM! Free Space Maps Decoded&lt;/A&gt; by Nikhil Sontakke&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://live.pgconf.in/schedule/160" target="_blank" rel="noopener"&gt;Journey of developing a Performance Optimization Feature in PostgreSQL&lt;/A&gt;, by Rahila Syed&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://live.pgconf.in/trainings" target="_blank" rel="noopener"&gt;Build Agentic AI with Semantic Kernel and Graph RAG on PostgreSQL&lt;/A&gt;, by Shriram Muthukrishnan &amp;amp; Palak Chaturvedi&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://live.pgconf.in/schedule/174" target="_blank" rel="noopener"&gt;All things Postgres @ Microsoft (2026 edition)&lt;/A&gt; by Sumedh Pathak&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Claire is an amazing speaker and has done a lot of work over the last several years documenting and understanding PostgreSQL committers and hackers. Her talk will definitely have some key insights and nuggets of information.&lt;/P&gt;
&lt;P&gt;Rahila’s talk will go in depth on performance optimization features and how best to test and benchmark them, and all the tools and tricks she has used as part of the feature development. This should be a must-see talk for anyone doing performance work.&lt;/P&gt;
&lt;H2&gt;Diving Deep: Case Studies &amp;amp; Technical Tracks&lt;/H2&gt;
&lt;P&gt;One of the tracks I’m really excited about is the Case Study track. I see these as similar to ‘Experience’ papers in academia. An experience paper documents what actually happened when applying a technique or system in the real world, what worked, what didn’t, and why. One of the talks I’m looking forward to is ‘&lt;A href="https://live.pgconf.in/schedule/139" target="_blank" rel="noopener"&gt;Operating Postgres Logical Replication at Massive Scale&lt;/A&gt;’ by Sai Srirampur from Clickhouse. Logical Replication is an extremely useful tool, and I’m curious to learn more about pitfalls and lessons learnt when running this at large scale. Another interesting one I’m curious to hear is &lt;A href="https://live.pgconf.in/schedule/101" target="_blank" rel="noopener"&gt;‘Understanding the importance of the commit log through a database corruption’&lt;/A&gt; by Amit Kumar Singh from EDB.&lt;/P&gt;
&lt;P&gt;The Database Engine Developers track allows us to go deep into the PostgreSQL code base and get a better understanding of how PostgreSQL is built. Even if you are not a database developer, this track is useful to understand how and why PostgreSQL does things, helping you be a better user of the database.&lt;/P&gt;
&lt;P&gt;With the rise of larger machines and memory available in the Cloud, different and newer memory architectures/tiers and serverless product offerings, there is a lot of deep dive in PostgreSQL’s memory architecture. There are some great talks focused on this area, which should be must-see for anyone interested in this topic:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;A href="https://live.pgconf.in/schedule/143" target="_blank" rel="noopener"&gt;Resizing shared buffer pool in a running PostgreSQL server: important, yet impossible&lt;/A&gt; by Ashutosh Bapat from Microsoft&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://live.pgconf.in/schedule/132" target="_blank" rel="noopener"&gt;From Disk to Data: Exploring PostgreSQL's Buffer Management&lt;/A&gt; by Lalit Choudhary from PurnaBIT&lt;/LI&gt;
&lt;LI&gt;&lt;A href="https://live.pgconf.in/schedule/114" target="_blank" rel="noopener"&gt;Beyond shared_buffers: On-Demand Memory in Modern PostgreSQL&lt;/A&gt; by Vaibhav Popat from Google&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;Finally, the Database Administration and Application Developer tracks have some really great content as well. They cover a wide range of topics, from PII data, HA/DR, Query Tuning to connection pooling and understanding conflict detection and resolution.&lt;/P&gt;
&lt;H2&gt;PostgreSQL in India: A Community Effort Worth Celebrating&lt;/H2&gt;
&lt;P&gt;Conferences like these are a rich source of information, dramatically increasing my personal understanding of the product and the ecosystem. Separately, they are also a great way to meet other practitioners in the space and connect with people in the industry. For people in Bangalore, another great option is the &lt;A href="https://www.pgblr.in/" target="_blank" rel="noopener"&gt;PostgreSQL Bangalore Meetup&lt;/A&gt;, and I’m super happy that Microsoft was able to join the ranks of other companies to &lt;A class="lia-external-url" href="https://www.linkedin.com/posts/nitin-jadhav-b4807950_what-an-incredible-postgres-bangalore-activity-7427227967803629568-78qg" target="_blank" rel="noopener"&gt;host the eighth iteration&lt;/A&gt; of this meetup.&lt;/P&gt;
&lt;P&gt;Finally, I would be remiss in not mentioning the hard work done by the &lt;A class="lia-external-url" href="https://live.pgconf.in/organisers" target="_blank" rel="noopener"&gt;PGConf India organizing team&lt;/A&gt; including Pavan Deolasse, Ashish Mehra, Nikhil Sontakke, Hari Kiran, and Rushabh Lathia who are making all of this happen. Also, a big shout out to the &lt;A class="lia-external-url" href="https://live.pgconf.in/paper-committee" target="_blank" rel="noopener"&gt;PGConf India Program Committee&lt;/A&gt; (Amul Sul, Dilip Kumar, Marc Linster, Thomas Munro, Vigneshwaran C) for putting together an amazing set of talks.&lt;/P&gt;
&lt;P&gt;I look forward to meeting all of you in Bangalore! Be sure to drop by the Microsoft booth to say hello (and to snag a free pair of our famous socks). I’d love to learn more about how you’re using Postgres.&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;</description>
      <pubDate>Tue, 24 Feb 2026 17:13:49 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/microsoft-at-pgconf-india-2026/ba-p/4496182</guid>
      <dc:creator>sumedhpathak</dc:creator>
      <dc:date>2026-02-24T17:13:49Z</dc:date>
    </item>
    <item>
      <title>Mastering gin_pending_list_limit: How This parameter shapes GIN index insert performance</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/mastering-gin-pending-list-limit-how-this-parameter-shapes-gin/ba-p/4494203</link>
      <description>&lt;P&gt;GIN (Generalized Inverted Index) indexes are a cornerstone of PostgreSQL when working with JSONB, arrays, and full-text search. They provide excellent read performance, but their write behavior—especially under sustained insert workloads—can vary dramatically depending on how data is primarily written and how GIN index maintenance is configured.&lt;/P&gt;
&lt;P&gt;One often-overlooked configuration in this space is the interaction between the&amp;nbsp;fastupdate&lt;STRONG&gt; &lt;/STRONG&gt;and gin_pending_list_limit server parameters. While these settings do not directly impact query performance, they play a critical role in insert throughput, CPU usage, and worst-case write latency for GIN-indexed tables.&lt;/P&gt;
&lt;P&gt;This post explains how the GIN pending list works, how gin_pending_list_limit governs its behavior, and why choosing the right configuration can make or break write performance on large datasets and write-intensive database operations.&lt;/P&gt;
&lt;P&gt;Let us now go through the 2 server parameters discussed above.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Fastupdate&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;The fastupdate storage parameter controls how PostgreSQL handles write to a GIN index:&lt;/P&gt;
&lt;P&gt;When fastupdate = ON (default)&lt;/P&gt;
&lt;P&gt;GIN uses an in-memory buffer containing the pending list of new index entries before flushing them into the main index structure.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;gin_pending_list_limit&lt;/STRONG&gt;&lt;BR /&gt;The gin_pending_list_limit parameter in PostgreSQL controls the maximum size of the pending list for a GIN (Generalized Inverted Index) before it's flushed to the main index structure. This setting can significantly affect insert performance and index maintenance behavior. By default, this parameter is set to 4 MB and by default.&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;New GIN entries are first written to the pending list.&lt;/LI&gt;
&lt;LI&gt;This makes inserts much faster because PostgreSQL batches write's and avoids expensive GIN maintenance on each individual insert.&lt;/LI&gt;
&lt;LI&gt;The pending list is later merged into the main GIN index by:
&lt;UL&gt;
&lt;LI&gt;Autovacuum or a VACUUM operation or&lt;/LI&gt;
&lt;LI&gt;When the pending list exceeds gin_pending_list_limit.&lt;/LI&gt;
&lt;/UL&gt;
&lt;/LI&gt;
&lt;LI&gt;During heavy insert workloads, these merges can cause latency spikes&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;In short, the pending list makes writes cheap—until the cleanup happens.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;How fastupdate Interacts With gin_pending_list_limit&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Together, these parameters decide how much index maintenance work each insert must perform.&lt;/P&gt;
&lt;P&gt;When fastupdate = ON&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Pending list absorbs writes efficiently → best for single inserts&lt;/LI&gt;
&lt;LI&gt;Flush cycles controlled by gin_pending_list_limit&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;When fastupdate = OFF&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Inserts bypass the pending list and write directly to the index&lt;/LI&gt;
&lt;LI&gt;This increases CPU costs dramatically during both single and batch inserts&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;Test run /results&lt;/STRONG&gt; &lt;STRONG&gt;and analysis&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Below are some tests run result’s based on different options highlighted in the above sections.&lt;/P&gt;
&lt;P&gt;A test table (of size 3 TB) uses a GIN index. We tested insert performance under different configurations of fastupdate (ON/OFF) for single and batch&lt;STRONG&gt; &lt;/STRONG&gt;inserts.&lt;BR /&gt;SKU: 16 vcore and 8 TB storage&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Setup:&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Create a table with a jsonb column and create a gin index on jsonb column.&lt;/LI&gt;
&lt;LI&gt;Insert around 2 TB of data&lt;/LI&gt;
&lt;LI&gt;setup an insert to perform single inserts for 15 minutes&lt;/LI&gt;
&lt;LI&gt;2nd run around setup an insert to perform batch inserts for 15 minutes&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;Key Factor:&lt;/STRONG&gt;&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Autovacuum &lt;EM&gt;was turned off&lt;/EM&gt;, so pending list cleanup did not occur automatically.&lt;/LI&gt;
&lt;LI&gt;Runs were captured over a 15-minute window&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;&lt;STRONG&gt;Single Inserts:&lt;/STRONG&gt;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&lt;STRONG&gt;Batch Inserts:&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;img /&gt;
&lt;P&gt;&amp;nbsp;&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Analysis:&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;Fastupdate &lt;STRONG&gt;ON&lt;/STRONG&gt; is optimal for &lt;EM&gt;single-row, write-heavy workloads&lt;/EM&gt; (much lower per-insert latency), but it hurts sustained throughput and worst-case latency due to pending-list cleanups with significantly lower CPU usage.&lt;BR /&gt;Fastupdate &lt;STRONG&gt;OFF&lt;/STRONG&gt; consistently wins for &lt;EM&gt;batch/bulk inserts&lt;/EM&gt;, delivering ~1.5–1.7× higher throughput, significantly lower max execution time and more predictable behavior despite higher CPU consumption, making it the better choice for controlled batch loads.&lt;/P&gt;
&lt;P&gt;&lt;STRONG&gt;Conclusion&lt;/STRONG&gt;&lt;/P&gt;
&lt;P&gt;GIN indexes are often treated as “build once and forget,” but for write-heavy systems, that mindset leaves significant performance on the table. By understanding how the pending list works—and tuning fastupdate and gin_pending_list_limit intentionally—you can dramatically improve both throughput and stability in large-scale PostgreSQL workloads.&lt;/P&gt;
&lt;P&gt;If you routinely work with heavy JSONB or array ingestion, these settings deserve a permanent spot in your performance toolbox.&lt;/P&gt;</description>
      <pubDate>Mon, 23 Feb 2026 17:03:18 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/mastering-gin-pending-list-limit-how-this-parameter-shapes-gin/ba-p/4494203</guid>
      <dc:creator>Gayathri_Paderla</dc:creator>
      <dc:date>2026-02-23T17:03:18Z</dc:date>
    </item>
    <item>
      <title>Distribute PostgreSQL 18 with Citus 14</title>
      <link>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/distribute-postgresql-18-with-citus-14/ba-p/4494868</link>
      <description>&lt;P&gt;The Citus 14.0 release is out and includes PostgreSQL 18 support! We know you've been waiting, and we've been hard at work adding features we believe will take your experience to the next level, focusing on bringing the&amp;nbsp;&lt;A href="https://www.postgresql.org/docs/18/release-18.html" target="_blank" rel="noopener"&gt;Postgres 18 exciting improvements&lt;/A&gt;&amp;nbsp;to you at distributed scale.&lt;/P&gt;
&lt;P&gt;The Citus database is an&amp;nbsp;&lt;A href="https://github.com/citusdata/citus" target="_blank" rel="noopener"&gt;open-source extension of Postgres&lt;/A&gt;&amp;nbsp;that brings the power of Postgres to any scale, from a single node to a distributed database cluster. Since Citus is an extension, using Citus means you're also using Postgres, giving you direct access to the Postgres features. And the latest of such features came with Postgres 18 release!&lt;/P&gt;
&lt;P&gt;PostgreSQL 18 is a substantial release: asynchronous I/O (AIO), skip-scan for multicolumn B-tree indexes,&amp;nbsp;uuidv7(), virtual generated columns by default, OAuth authentication,&amp;nbsp;&lt;EM&gt;RETURNING OLD/NEW&lt;/EM&gt;, and temporal constraints. For those of you who are interested in upgrading to Postgres 18 and scaling these new features of Postgres: you can upgrade to Citus 14.0!&lt;/P&gt;
&lt;P&gt;Let's take a closer look at what's new in Citus 14.0.&lt;/P&gt;
&lt;H2&gt;Postgres 18 support in Citus 14.0&lt;/H2&gt;
&lt;P&gt;Citus 14.0 introduces support for PostgreSQL 18. This means that just by enabling PG18 in Citus 14.0, all the query performance improvements directly reflect on the Citus distributed queries, and several optimizer improvements benefit queries in Citus out of the box! Among the many new features in PG 18, the following capabilities enabled in Citus 14.0 are especially noteworthy for Citus users.&lt;/P&gt;
&lt;P&gt;To learn more about how you can use Citus 14.0 + PostgreSQL 18, as well as currently unsupported features and future work, you can consult the&amp;nbsp;Citus 14.0 Updates page, which gives you detailed release notes.&lt;/P&gt;
&lt;H2&gt;PostgreSQL 18 highlights that benefit Citus clusters&lt;/H2&gt;
&lt;P&gt;Because Citus is implemented as a Postgres extension, the following PG18 improvements benefit your distributed cluster&amp;nbsp;&lt;STRONG&gt;automatically, &lt;/STRONG&gt;no Citus-specific changes needed.&lt;/P&gt;
&lt;H3&gt;Faster scans and maintenance via AIO&lt;/H3&gt;
&lt;P&gt;Postgres 18 adds an&amp;nbsp;&lt;STRONG&gt;asynchronous I/O subsystem&lt;/STRONG&gt;&amp;nbsp;that can improve sequential scans, bitmap heap scans, and vacuuming—workloads that show up constantly in shard-heavy distributed clusters. This means your Citus cluster can benefit from faster table scans and more efficient maintenance operations without any code changes.&lt;/P&gt;
&lt;P&gt;You can control the I/O method via the new&amp;nbsp;&lt;EM&gt;io_method&amp;nbsp;&lt;/EM&gt;GUC:&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;-- Check the current I/O method
SHOW io_method;&lt;/LI-CODE&gt;
&lt;H3&gt;Better index usage with skip-scan&lt;/H3&gt;
&lt;P&gt;Postgres 18 expands when&amp;nbsp;&lt;STRONG&gt;multicolumn B-tree indexes&lt;/STRONG&gt;&amp;nbsp;can be used via&amp;nbsp;&lt;STRONG&gt;skip scan&lt;/STRONG&gt;, helping common multi-tenant schemas where predicates don't always constrain the leading index column. This is particularly valuable for Citus users with multi-tenant applications where queries often filter by non-leading columns.&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;-- Multi-tenant index: (tenant_id, created_at) 
-- PG18 skip-scan lets this query use the index even without tenant_id 
SELECT * FROM events 
WHERE created_at &amp;gt; now() - interval '1 day' 
ORDER BY created_at 
DESC LIMIT 100;&lt;/LI-CODE&gt;
&lt;H3&gt;uuidv7() for time-ordered UUIDs&lt;/H3&gt;
&lt;P&gt;Time-ordered UUIDs can reduce index churn and improve locality; Postgres 18 adds&amp;nbsp;uuidv7(). This is especially useful for distributed tables where you want predictable ordering and better index performance across shards.&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;-- Use uuidv7() as a time-ordered primary key 
CREATE TABLE events (
   id uuid DEFAULT uuidv7() PRIMARY KEY, 
   tenant_id bigint, 
   payload jsonb 
);
 
SELECT create_distributed_table('events', 'tenant_id');&lt;/LI-CODE&gt;
&lt;H3&gt;OAuth authentication support&lt;/H3&gt;
&lt;P&gt;Postgres 18 adds&amp;nbsp;&lt;STRONG&gt;OAuth authentication&lt;/STRONG&gt;, making it easier to plug database auth into modern SSO flows often a practical requirement in multi-node deployments. This simplifies authentication management across your Citus coordinator and worker nodes.&lt;/P&gt;
&lt;H2&gt;What Citus 14 adds for PostgreSQL 18 compatibility&lt;/H2&gt;
&lt;P&gt;While the highlights above work out of the box, PG18 also introduces&amp;nbsp;&lt;STRONG&gt;new SQL syntax and behavior changes&lt;/STRONG&gt; that require Citus-specific work parsing/deparsing, DDL propagation across coordinator + workers, and distributed execution correctness. Here's what we built to make these work end-to-end.&lt;/P&gt;
&lt;H3&gt;JSON_TABLE() COLUMNS&lt;/H3&gt;
&lt;P&gt;PG18 expands SQL/JSON&amp;nbsp;&lt;EM&gt;JSON_TABLE()&lt;/EM&gt;&amp;nbsp;with a richer&amp;nbsp;&lt;EM&gt;COLUMNS&amp;nbsp;&lt;/EM&gt;clause, making it easy to extract multiple fields from JSON documents in a single, typed table expression. Citus 14 ensures the syntax can be parsed/deparsed and executed consistently in distributed queries.&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;CREATE TABLE pg18_json_test (id serial PRIMARY KEY, data JSON);

SELECT jt.name, jt.age
FROM pg18_json_test,
     JSON_TABLE(
       data,
       '$.user'
       COLUMNS (
         age  INT  PATH '$.age',
         name TEXT PATH '$.name'
       )
     ) AS jt
WHERE jt.age BETWEEN 25 AND 35
ORDER BY jt.age, jt.name;&lt;/LI-CODE&gt;
&lt;H3&gt;Temporal constraints&lt;/H3&gt;
&lt;P&gt;Postgres 18 adds temporal constraint syntax that Citus must propagate and preserve correctly:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;EM&gt;WITHOUT OVERLAPS&lt;/EM&gt;&amp;nbsp;for&amp;nbsp;&lt;EM&gt;PRIMARY KEY&lt;/EM&gt;&amp;nbsp;/&amp;nbsp;&lt;EM&gt;UNIQUE&lt;/EM&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;EM&gt;PERIOD&amp;nbsp;&lt;/EM&gt;for&amp;nbsp;&lt;EM&gt;FOREIGN KEY&lt;/EM&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;LI-CODE lang="sql"&gt;CREATE TABLE temporal_rng (
  id int4range,
  valid_at daterange,
  CONSTRAINT temporal_rng_pk PRIMARY KEY (id, valid_at WITHOUT OVERLAPS)
);

SELECT create_distributed_table('temporal_rng', 'id');&lt;/LI-CODE&gt;
&lt;H3&gt;CREATE FOREIGN TABLE ... LIKE&lt;/H3&gt;
&lt;P&gt;Postgres 18 supports&amp;nbsp;&lt;EM&gt;CREATE FOREIGN TABLE ... LIKE&lt;/EM&gt;, letting you define a foreign table by copying the column layout (and optionally defaults/constraints/indexes) from an existing table. Citus 14 includes coverage so FDW workflows remain compatible in distributed environments.&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;-- Copy column layout from an existing table
CREATE FOREIGN TABLE my_ft (LIKE my_local_table EXCLUDING ALL)
  SERVER foreign_server
  OPTIONS (schema_name 'public', table_name 'my_local_table');&lt;/LI-CODE&gt;
&lt;H3&gt;Generated columns (Virtual by Default)&lt;/H3&gt;
&lt;P&gt;PostgreSQL 18 changes generated column behavior significantly:&lt;/P&gt;
&lt;OL&gt;
&lt;LI&gt;&lt;STRONG&gt;Virtual by default:&lt;/STRONG&gt;&amp;nbsp;Generated columns are now computed on read rather than stored, reducing write amplification&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;Logical replication support:&lt;/STRONG&gt;&amp;nbsp;New&amp;nbsp;&lt;EM&gt;publish_generated_columns&lt;/EM&gt;&amp;nbsp;publication option for replicating generated values&lt;/LI&gt;
&lt;/OL&gt;
&lt;LI-CODE lang="sql"&gt;CREATE TABLE events (
  id bigint,
  payload jsonb,
  payload_hash text GENERATED ALWAYS AS (md5(payload::text)) VIRTUAL
);

SELECT create_distributed_table('events', 'id');&lt;/LI-CODE&gt;
&lt;H3&gt;VACUUM/ANALYZE ONLY semantics&lt;/H3&gt;
&lt;P&gt;Postgres 18 introduces&amp;nbsp;ONLY&amp;nbsp;for&amp;nbsp;&lt;EM&gt;VACUUM&amp;nbsp;&lt;/EM&gt;and&amp;nbsp;&lt;EM&gt;ANALYZE&amp;nbsp;&lt;/EM&gt;so you can explicitly&amp;nbsp;&lt;STRONG&gt;target only the parent&lt;/STRONG&gt;&amp;nbsp;of a partitioned/inheritance tree without automatically processing children. Citus 14 adapts distributed utility-command behavior so&amp;nbsp;&lt;EM&gt;ONLY&amp;nbsp;&lt;/EM&gt;works as intended.&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;-- Parent-only: do not recurse into partitions/children
VACUUM (ANALYZE) ONLY metrics;
ANALYZE ONLY metrics;&lt;/LI-CODE&gt;
&lt;H3&gt;Constraints: NOT ENFORCED + partitioned-table additions&lt;/H3&gt;
&lt;P&gt;Postgres 18 expands constraint syntax in several ways that Citus must parse/deparse and propagate across coordinator + workers:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;EM&gt;CHECK&amp;nbsp;&lt;/EM&gt;constraints can be marked&amp;nbsp;&lt;EM&gt;NOT ENFORCED&lt;/EM&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;EM&gt;FOREIGN KEY&lt;/EM&gt;&amp;nbsp;constraints can be marked&amp;nbsp;&lt;EM&gt;NOT ENFORCED&lt;/EM&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;EM&gt;NOT VALID&lt;/EM&gt;&amp;nbsp;foreign keys on partitioned tables&lt;/LI&gt;
&lt;LI&gt;&lt;EM&gt;DROP CONSTRAINT ONLY&lt;/EM&gt;&amp;nbsp;on partitioned tables&lt;/LI&gt;
&lt;/UL&gt;
&lt;LI-CODE lang="sql"&gt;ALTER TABLE orders
  ADD CONSTRAINT orders_amount_positive CHECK (amount &amp;gt; 0) NOT ENFORCED;

ALTER TABLE orders
  ADD CONSTRAINT orders_customer_fk
  FOREIGN KEY (customer_id) REFERENCES customers(id)
  NOT ENFORCED;&lt;/LI-CODE&gt;
&lt;H3&gt;DML: RETURNING OLD/NEW&lt;/H3&gt;
&lt;P&gt;Postgres 18 lets&amp;nbsp;&lt;EM&gt;RETURNING&amp;nbsp;&lt;/EM&gt;reference both the&amp;nbsp;&lt;STRONG&gt;previous&lt;/STRONG&gt;&amp;nbsp;(&lt;EM&gt;old&lt;/EM&gt;) and&amp;nbsp;&lt;STRONG&gt;new&lt;/STRONG&gt;&amp;nbsp;(&lt;EM&gt;new&lt;/EM&gt;) row values in&amp;nbsp;INSERT/UPDATE/DELETE/MERGE. Citus 14 preserves these semantics in distributed execution.&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;UPDATE t
SET v = v + 1
WHERE id = 42
RETURNING old.v AS old_v, new.v AS new_v;&lt;/LI-CODE&gt;
&lt;H3&gt;COPY expansions&lt;/H3&gt;
&lt;P&gt;PG18 adds two useful COPY improvements that Citus 14 supports in distributed queries:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;EM&gt;&lt;STRONG&gt;COPY ... REJECT_LIMIT&lt;/STRONG&gt;&lt;/EM&gt;: set a threshold for how many rows can be rejected before the COPY fails, useful for resilient bulk loading into sharded tables&lt;/LI&gt;
&lt;LI&gt;&lt;STRONG&gt;&lt;EM&gt;COPY table TO&lt;/EM&gt;&amp;nbsp;from materialized views&lt;/STRONG&gt;: export data directly from materialized views&lt;/LI&gt;
&lt;/UL&gt;
&lt;LI-CODE lang="sql"&gt;-- Tolerate up to 10 bad rows during bulk load
COPY my_distributed_table FROM '/data/import.csv'
  WITH (FORMAT csv, REJECT_LIMIT 10);&lt;/LI-CODE&gt;
&lt;H3&gt;MIN()/MAX() on arrays and composite types&lt;/H3&gt;
&lt;P&gt;PG18 extends&amp;nbsp;&lt;EM&gt;MIN()&lt;/EM&gt;&amp;nbsp;and&amp;nbsp;&lt;EM&gt;MAX()&lt;/EM&gt;&amp;nbsp;aggregates to work on arrays and composite types. Citus 14 ensures these aggregates work correctly in distributed queries.&lt;/P&gt;
&lt;LI-CODE lang="sql"&gt;CREATE TABLE sensor_data (
  tenant_id bigint,
  readings int[]
);
SELECT create_distributed_table('sensor_data', 'tenant_id');

-- Now works with array columns
SELECT MIN(readings), MAX(readings) FROM sensor_data;&lt;/LI-CODE&gt;
&lt;H3&gt;Nondeterministic collations&lt;/H3&gt;
&lt;P&gt;PG18 extends&amp;nbsp;&lt;EM&gt;LIKE&amp;nbsp;&lt;/EM&gt;and text-position search functions to work with nondeterministic collations. Citus 14 verifies these work correctly across distributed queries.&lt;/P&gt;
&lt;H3&gt;sslkeylogfile connection parameter&lt;/H3&gt;
&lt;P&gt;PG18 adds the&amp;nbsp;&lt;EM&gt;sslkeylogfile&amp;nbsp;libpq&lt;/EM&gt; connection parameter for dumping SSL key material, useful for debugging encrypted connections. Citus 14 allows configuring this via&amp;nbsp;&lt;EM&gt;citus.node_conn_info&lt;/EM&gt;&amp;nbsp;so it works across inter-node connections.&lt;/P&gt;
&lt;H3&gt;Planner fix: enable_self_join_elimination&lt;/H3&gt;
&lt;P&gt;PG18 introduces the&amp;nbsp;&lt;EM&gt;enable_self_join_elimination&amp;nbsp;&lt;/EM&gt;planner optimization. Citus 14 ensures this works correctly for joins between distributed and local tables, avoiding wrong results that could occur in early PG18 integration.&lt;/P&gt;
&lt;H3&gt;Utility/Ops plumbing and observability&lt;/H3&gt;
&lt;P&gt;Citus 14 adapts to PG18 interface/output changes that affect tooling and extension plumbing:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;New GUC&amp;nbsp;&lt;EM&gt;file_copy_method&amp;nbsp;&lt;/EM&gt;for&amp;nbsp;&lt;EM&gt;CREATE DATABASE ... STRATEGY=FILE_COPY&lt;/EM&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;EM&gt;EXPLAIN (WAL&lt;/EM&gt;)&amp;nbsp;adds a "WAL buffers full" field; Citus propagates it through distributed EXPLAIN output&lt;/LI&gt;
&lt;LI&gt;New extension macro&amp;nbsp;&lt;EM&gt;PG_MODULE_MAGIC_EXT&lt;/EM&gt;&amp;nbsp;so extensions can report name/version metadata&lt;/LI&gt;
&lt;LI&gt;New &lt;EM&gt;libpq&lt;/EM&gt; parameter&amp;nbsp;&lt;EM&gt;sslkeylogfile&lt;/EM&gt;&amp;nbsp;support via&amp;nbsp;&lt;EM&gt;citus.node_conn_info&lt;/EM&gt;&lt;/LI&gt;
&lt;/UL&gt;
&lt;H2&gt;Diving deeper into Citus 14.0 and distributed Postgres&lt;/H2&gt;
&lt;P&gt;To learn more about Citus 14.0, you can:&lt;/P&gt;
&lt;UL&gt;
&lt;LI&gt;Check out the&amp;nbsp;&lt;A href="https://github.com/citusdata/citusdata.com/blob/2fabdda10a900ffdf12f71226e2b960c46937ae0/updates/v14-0" target="_blank" rel="noopener"&gt;14.0 Updates page&lt;/A&gt;&amp;nbsp;to get the detailed release notes.&lt;/LI&gt;
&lt;LI&gt;As of this release, &lt;A href="https://learn.microsoft.com/postgresql/citus/?view=citus-14" target="_blank"&gt;Citus documentation&lt;/A&gt; is now hosted on Microsoft Learn.&lt;/LI&gt;
&lt;/UL&gt;
&lt;P&gt;With Citus 14, &lt;A class="lia-internal-link lia-internal-url lia-internal-url-content-type-blog" href="https://techcommunity.microsoft.com/blog/adforpostgresql/postgresql-for-the-enterprise-scale-secure-simplify/4470412#community-4470412-ec" data-lia-auto-title="elastic clusters" data-lia-auto-title-active="0" target="_blank"&gt;elastic clusters&lt;/A&gt; will soon support PostgreSQL 18, now available in Azure Database for PostgreSQL.&amp;nbsp;&lt;/P&gt;
&lt;P&gt;You can stay connected on the &lt;A href="https://slack.citusdata.com/" target="_blank" rel="noopener"&gt;Citus Slack&lt;/A&gt;&amp;nbsp;and visit the&amp;nbsp;&lt;A href="https://github.com/citusdata/citus" target="_blank" rel="noopener"&gt;Citus open source GitHub repo&lt;/A&gt; to see recent developments as well. If there's something you'd like to see next in Citus, feel free to also open a feature request issue :)&lt;/P&gt;</description>
      <pubDate>Tue, 17 Feb 2026 16:19:55 GMT</pubDate>
      <guid>https://techcommunity.microsoft.com/t5/microsoft-blog-for-postgresql/distribute-postgresql-18-with-citus-14/ba-p/4494868</guid>
      <dc:creator>mehmetyilmaz</dc:creator>
      <dc:date>2026-02-17T16:19:55Z</dc:date>
    </item>
  </channel>
</rss>

