<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Franck Pachot</title>
    <description>The latest articles on DEV Community by Franck Pachot (@franckpachot).</description>
    <link>https://dev.to/franckpachot</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F114176%2F085eac75-4d8d-473b-b0a9-5a4f46c54931.jpg</url>
      <title>DEV Community: Franck Pachot</title>
      <link>https://dev.to/franckpachot</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/franckpachot"/>
    <language>en</language>
    <item>
      <title>Patroni, Ubuntu 24.04, and two unplanned PostgreSQL failovers</title>
      <dc:creator>Franck Pachot</dc:creator>
      <pubDate>Wed, 04 Nov 2026 13:33:50 +0000</pubDate>
      <link>https://dev.to/franckpachot/patroni-ubuntu-2404-and-two-unplanned-postgresql-failovers-52gl</link>
      <guid>https://dev.to/franckpachot/patroni-ubuntu-2404-and-two-unplanned-postgresql-failovers-52gl</guid>
      <description>&lt;p&gt;I usually run PostgreSQL on managed services, like &lt;a href="https://azure.microsoft.com/en-gb/products/postgresql" rel="noopener noreferrer"&gt;Azure Database for PostgreSQL Flexible Server&lt;/a&gt; but for once I was running PostgreSQL/Patroni on VMs. An overnight data-loading operation had been running for eight hours when its PostgreSQL connections were lost. The client-side symptoms were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SSL connection has been closed unexpectedly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An SSL error is a symptom, not a diagnosis. PostgreSQL had not run out of memory or disk. The server was being stopped by service management. Neither PostgreSQL nor Patroni needed a package upgrade, but an unattended zlib shared-library update caused the issue.&lt;/p&gt;

&lt;p&gt;Ubuntu 24.04's &lt;code&gt;needrestart&lt;/code&gt; automatically restarted affected services, including my custom Patroni service. The primary stopped, another node was promoted, and the load failed. Then the new primary received its own unattended updates and stopped too, causing a second failover.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was running
&lt;/h2&gt;

&lt;p&gt;This was a self-managed PostgreSQL 17/Patroni lab on Azure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Three PostgreSQL nodes: pg1, pg2 and pg3, across three availability zones.&lt;/li&gt;
&lt;li&gt;Three separate etcd nodes for Patroni's distributed configuration store.&lt;/li&gt;
&lt;li&gt;One application client VM.&lt;/li&gt;
&lt;li&gt;Canonical Ubuntu 24.04 LTS guests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hostnames, service prefixes, and local policy filenames below are consistently anonymized. Timestamps, package versions, and database messages are unchanged.&lt;/p&gt;

&lt;p&gt;The deployment record identifies the image as &lt;code&gt;Canonical:ubuntu-24_04-lts:server:latest&lt;/code&gt;, resolved to &lt;code&gt;24.04.202610210&lt;/code&gt;, with Azure &lt;code&gt;patchMode=ImageDefault&lt;/code&gt; and &lt;code&gt;assessmentMode=ImageDefault&lt;/code&gt;. Those settings did not disable maintenance already configured inside Ubuntu.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root cause analysis
&lt;/h2&gt;

&lt;p&gt;I correlated PostgreSQL, Patroni, and systemd messages from the same journal. PostgreSQL's stderr was captured under the Patroni unit here. A &lt;code&gt;patroni[PID]&lt;/code&gt; journal prefix does not mean every message was generated by Patroni itself: the PostgreSQL &lt;code&gt;LOG&lt;/code&gt; and &lt;code&gt;FATAL&lt;/code&gt; messages identify the database side.&lt;/p&gt;

&lt;p&gt;On pg1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;--utc&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; short-iso &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s1"&gt;'2026-11-04 06:18:50 UTC'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--until&lt;/span&gt; &lt;span class="s1"&gt;'2026-11-04 06:20:20 UTC'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; franckpachot-lab-patroni.service |
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'Stopping |Stopped |Started |shutdown request|administrator command'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Selected captured lines, with repeated connection-termination messages omitted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-11-04T06:19:04+00:00 franckpachot-lab-pg1-kc-20261103 patroni[46600]: 2026-11-04 06:19:04 UTC-6aea534b.b608-LOG:  received smart shutdown request
2026-11-04T06:19:04+00:00 franckpachot-lab-pg1-kc-20261103 patroni[46623]: 2026-11-04 06:19:04 UTC-6aea534c.b61f-FATAL:  terminating connection due to administrator command
2026-11-04T06:19:04+00:00 franckpachot-lab-pg1-kc-20261103 systemd[1]: Stopping franckpachot-lab-patroni.service - franckpachot-lab PostgreSQL Patroni quorum lab...
2026-11-04T06:19:06+00:00 franckpachot-lab-pg1-kc-20261103 patroni[46600]: 2026-11-04 06:19:06 UTC-6aea534b.b608-LOG:  received fast shutdown request
2026-11-04T06:20:14+00:00 franckpachot-lab-pg1-kc-20261103 systemd[1]: Stopped franckpachot-lab-patroni.service - franckpachot-lab PostgreSQL Patroni quorum lab.
2026-11-04T06:20:14+00:00 franckpachot-lab-pg1-kc-20261103 systemd[1]: Started franckpachot-lab-patroni.service - franckpachot-lab PostgreSQL Patroni quorum lab.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is positive evidence of an administered shutdown and a service restart, not merely an inference from lost connections. "Administrator command" does not identify a human: automated service management can produce it.&lt;/p&gt;

&lt;p&gt;There is a service-unit detail worth checking as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl show franckpachot-lab-patroni.service &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; KillMode &lt;span class="nt"&gt;-p&lt;/span&gt; KillSignal

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The follow-up inspection returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;KillMode=control-group
KillSignal=15

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My custom unit had no explicit &lt;code&gt;KillMode&lt;/code&gt; setting. With &lt;code&gt;control-group&lt;/code&gt;, systemd's stop signal is not restricted to the Patroni parent process. Service-unit shutdown semantics matter too. I should not assume that restarting the supervisor leaves PostgreSQL untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove that unattended upgrades actually ran
&lt;/h2&gt;

&lt;p&gt;The familiar status command was a useful starting point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status unattended-upgrades.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before remediation, it showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Loaded: loaded (/usr/lib/systemd/system/unattended-upgrades.service; enabled; preset: enabled)
Active: active (running) since Tue 2026-11-03 16:25:04 UTC; 15h ago
Main PID: 1641 (unattended-upgr)
/usr/bin/python3 /usr/share/unattended-upgrades/unattended-upgrade-shutdown --wait-for-signal

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not prove packages were being installed at that moment. That particular process waits to coordinate shutdown with unattended upgrades. The decisive evidence comes from the scheduled service and transaction logs.&lt;/p&gt;

&lt;p&gt;On pg1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;--utc&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; short-iso &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s1"&gt;'2026-11-04 06:10:00 UTC'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--until&lt;/span&gt; &lt;span class="s1"&gt;'2026-11-04 06:25:00 UTC'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; apt-daily-upgrade.service

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Selected captured lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-11-04T06:18:58+00:00 franckpachot-lab-pg1-kc-20261103 systemd[1]: Starting apt-daily-upgrade.service - Daily apt upgrade and clean activities...
2026-11-04T06:20:31+00:00 franckpachot-lab-pg1-kc-20261103 systemd[1]: Finished apt-daily-upgrade.service - Daily apt upgrade and clean activities.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unattended-upgrades log supplies the package list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^2026-11-04 06:.*(Starting unattended|Packages that will be upgraded|All upgrades installed)'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /var/log/unattended-upgrades/unattended-upgrades.log

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first transaction's relevant lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-11-04 06:18:58,776 INFO Starting unattended upgrades script
2026-11-04 06:19:02,465 INFO Packages that will be upgraded: bind9-dnsutils bind9-host bind9-libs libbrotli1 libmspack0t64 libnss3 libpcre2-8-0 zlib1g
2026-11-04 06:20:30,481 INFO All upgrades installed

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was automated package maintenance in the exact incident window.&lt;/p&gt;

&lt;h2&gt;
  
  
  What package changed, and when?
&lt;/h2&gt;

&lt;p&gt;The ordinary dpkg database log gives timestamped package actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^2026-11-04 06:19:.*(upgrade zlib1g|configure zlib1g)'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /var/log/dpkg.log

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Captured output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-11-04 06:19:03 upgrade zlib1g:amd64 1:1.3.dfsg-3.1ubuntu2.2 1:1.3.dfsg-3.1ubuntu2.3
2026-11-04 06:19:03 configure zlib1g:amd64 1:1.3.dfsg-3.1ubuntu2.3 &amp;lt;none&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This proves dpkg performed the package upgrade/configuration. It is not a process-audit record of dpkg's full command-line arguments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where did the &lt;code&gt;systemctl restart&lt;/code&gt; command come from?
&lt;/h2&gt;

&lt;p&gt;Not from &lt;code&gt;/var/log/dpkg.log&lt;/code&gt;. The literal command appears in the captured package-manager output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'/^Log started: 2026-11-04  *06:19:02/,/^Log ended:/p'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /var/log/unattended-upgrades/unattended-upgrades-dpkg.log

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Selected lines from that transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Log started: 2026-11-04  06:19:02
Preparing to unpack .../zlib1g_1%3a1.3.dfsg-3.1ubuntu2.3_amd64.deb ...
Setting up zlib1g:amd64 (1:1.3.dfsg-3.1ubuntu2.3) ...
Restarting services...
 systemctl restart azuremonitor-coreagent.service franckpachot-lab-host-metrics.service franckpachot-lab-patroni.service fwupd.service gcd.service polkit.service rsyslog.service ssh.service udisks2.service walinuxagent.service
Log ended: 2026-11-04  06:20:15

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The custom Patroni service is explicitly named.&lt;/p&gt;

&lt;p&gt;These logs also print commands under a deferred-restarts heading elsewhere. A bare grep for &lt;code&gt;systemctl restart&lt;/code&gt; is not sufficient: inspect the heading and surrounding transaction. Here the Patroni command is under &lt;code&gt;Restarting services...&lt;/code&gt;, and systemd independently records that exact&lt;br&gt;
service stopping and starting.&lt;/p&gt;

&lt;p&gt;The output does not timestamp each printed command. I assign 06:19:04 to the observed Patroni stop/PostgreSQL shutdown from the journal.&lt;/p&gt;

&lt;p&gt;Together, the transaction output and systemd journal establish the restart path. I did not have an &lt;code&gt;auditd&lt;/code&gt;/&lt;code&gt;execve&lt;/code&gt; trace recording every parent PID and argument, so this is log correlation, not a reconstructed process audit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why would a zlib upgrade restart Patroni?
&lt;/h2&gt;

&lt;p&gt;The installed APT hook answers that question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/apt/apt.conf.d/99needrestart

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its active configuration line was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DPkg::Post-Invoke {"test -x /usr/lib/needrestart/apt-pinvoke &amp;amp;&amp;amp; /usr/lib/needrestart/apt-pinvoke -m u || true"; };

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The invoked script ultimately executes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    exec /usr/sbin/needrestart "$@"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;needrestart&lt;/code&gt; checks running processes for outdated shared libraries. A service can therefore be selected without its own package being upgraded. It is not simply walking package dependencies.&lt;/p&gt;

&lt;p&gt;The explanation was already installed on the VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /usr/share/doc/needrestart/README.Ubuntu

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first paragraph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Starting from Ubuntu 24.04, needrestart in the default configuration will by default not show the Debconf prompt, and restart automatically the affected services. This includes when run as part of `unattended-upgrades` to ensure that the security updates it brings are actually applied to running services
rather than leaving the system vulnerable.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a documented Ubuntu 24.04 behavior, not a claim about every Ubuntu release or every customized Azure image. Canonical describes the change and its security rationale in&lt;br&gt;
&lt;a href="https://discourse.ubuntu.com/t/needrestart-changes-in-ubuntu-24-04-service-restarts/44671" rel="noopener noreferrer"&gt;needrestart changes in Ubuntu 24.04: service restarts&lt;/a&gt;. While this may be great for services that are resilient to restarts, it's probably not a good idea for a monolithic database such as PostgreSQL, where restarting the primary interrupts all client sessions.&lt;/p&gt;

&lt;p&gt;Updating a library on disk does not update the copy already mapped into a running process. Restarting affected services applies the fix. The missing piece in my setup was coordinating that legitimate maintenance with HA and a long-running load.&lt;/p&gt;
&lt;h2&gt;
  
  
  But why would Patroni have zlib loaded?
&lt;/h2&gt;

&lt;p&gt;A direct &lt;code&gt;import zlib&lt;/code&gt; in Patroni's own source is not required. Patroni runs inside Python, and imported modules or native dependencies can load shared libraries transitively. Python's &lt;code&gt;zlib&lt;/code&gt; extension is one possible route: modules such as &lt;code&gt;gzip&lt;/code&gt; and &lt;code&gt;zipfile&lt;/code&gt; can use it. A native dependency of another&lt;br&gt;
component in the service is another possibility.&lt;/p&gt;

&lt;p&gt;However, being a Python process does not automatically mean that zlib is loaded at startup. On typical distribution builds, the Python &lt;code&gt;zlib&lt;/code&gt; module is a dynamically loaded extension, not evidence that the interpreter itself unconditionally links and loads &lt;code&gt;libz&lt;/code&gt;. The actual imports and native dependencies matter.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;needrestart&lt;/code&gt; examines running processes, including their memory mappings, to identify obsolete library files and associate affected processes with services. It does not need to prove that application code is actively calling the library. A process can keep an old library mapped after an upgrade replaces the on-disk file.&lt;/p&gt;

&lt;p&gt;There is another distinction: the Patroni systemd service also contains PostgreSQL processes. Selecting that unit does not, by itself, prove that the Patroni Python parent, rather than a process it manages, held the obsolete mapping.&lt;/p&gt;

&lt;p&gt;For a running service, investigate with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;systemctl show franckpachot-lab-patroni.service &lt;span class="nt"&gt;-p&lt;/span&gt; MainPID &lt;span class="nt"&gt;--value&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;sudo grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'libz\.so|deleted'&lt;/span&gt; &lt;span class="s2"&gt;"/proc/&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;/maps"&lt;/span&gt;
systemctl status franckpachot-lab-patroni.service &lt;span class="nt"&gt;--no-pager&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inspect the mappings of the other processes in the service as well. Use &lt;code&gt;ldd&lt;/code&gt; on the relevant executable or native extension to investigate library dependencies, remembering that &lt;code&gt;ldd&lt;/code&gt; alone does not list every library loaded later at runtime. These are diagnostic commands, not captured historical output: after a restart, the old mappings are gone.&lt;/p&gt;

&lt;p&gt;The retained evidence establishes: zlib upgrade -&amp;gt; immediate restart list containing Patroni -&amp;gt; observed service shutdown -&amp;gt; failover. It does not include a pre-restart memory-map snapshot identifying the precise importing module or process, so I would not present a particular Python dependency chain as proven.&lt;/p&gt;

&lt;p&gt;Nor would I explain it as zlib enabling compressed PostgreSQL connections: PostgreSQL's standard client/server protocol does not provide general-purpose compression of that kind. Any proposed &lt;code&gt;libpq&lt;/code&gt; dependency path needs evidence from the installed build, not that assumption.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about other releases and distributions?
&lt;/h2&gt;

&lt;p&gt;There are three separate policy questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Are updates checked or downloaded automatically?&lt;/li&gt;
&lt;li&gt;Are packages actually installed automatically?&lt;/li&gt;
&lt;li&gt;Which running services are restarted afterward, and by whom?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A timer being enabled does not answer all three. Neither does an&lt;br&gt;
&lt;code&gt;Automatic-Reboot&lt;/code&gt; setting.&lt;/p&gt;
&lt;h3&gt;
  
  
  Ubuntu before and after 24.04
&lt;/h3&gt;

&lt;p&gt;Canonical's &lt;a href="https://discourse.ubuntu.com/t/needrestart-changes-in-ubuntu-24-04-service-restarts/44671" rel="noopener noreferrer"&gt;description of the 24.04 change&lt;/a&gt; explicitly contrasts it with the previous &lt;code&gt;needrestart&lt;/code&gt; behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before 24.04&lt;/strong&gt;, the APT-integrated tool prompted for service restarts in interactive use. In noninteractive use, it listed affected services without restarting them. This is the pre-Noble behavior relevant when comparing older releases such as 20.04 and 22.04, where the tool is installed and configured that way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ubuntu 24.04&lt;/strong&gt; changed that APT-hook behavior to restart affected services directly in both contexts, including unattended upgrades. That is the behavior documented and observed on these VMs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other or later releases and customized images&lt;/strong&gt; need their own checks. An upgraded host can retain configuration overrides, and a cloud image can add automation. Do not infer the effective policy from the release name alone.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The older behavior was not a promise that package upgrades could never restart a service: package maintainer scripts and other automation are separate paths. An older release also doesn't mean unattended security updates were disabled. The relevant change here is what happens to affected running processes after an APT transaction.&lt;/p&gt;

&lt;p&gt;On an Ubuntu or Debian host, inspect rather than guess:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/os-release
dpkg-query &lt;span class="nt"&gt;-W&lt;/span&gt; unattended-upgrades needrestart
apt-config dump | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'APT::Periodic|Unattended-Upgrade::Automatic-Reboot'&lt;/span&gt;
systemctl list-timers &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="s1"&gt;'apt-*'&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/apt/apt.conf.d/99needrestart
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'nrconf.*restart'&lt;/span&gt; /etc/needrestart

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Debian, RHEL and Fedora
&lt;/h3&gt;

&lt;p&gt;These are documented policy differences:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Automatic package installation&lt;/th&gt;
&lt;th&gt;What to check for service disruption&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Debian, including 12/13 deployments&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;unattended-upgrades&lt;/code&gt; is controlled by APT periodic configuration and installer/image choices. Do not assume every installation enables it.&lt;/td&gt;
&lt;td&gt;Inspect the installed &lt;code&gt;needrestart&lt;/code&gt; version, mode, hooks, and package scripts. Sharing APT does not establish that Ubuntu's specific &lt;code&gt;-m u&lt;/code&gt; policy applies.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RHEL 9&lt;/td&gt;
&lt;td&gt;Red Hat documents installing &lt;code&gt;dnf-automatic&lt;/code&gt; and enabling a timer. By default, it downloads and reports updates without installing them.&lt;/td&gt;
&lt;td&gt;The selected timer matters: &lt;code&gt;dnf-automatic-install.timer&lt;/code&gt; explicitly installs updates and overrides the corresponding configuration options. Inspect package scripts and any external maintenance tooling too.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fedora 41 and newer, traditional DNF-based systems&lt;/td&gt;
&lt;td&gt;Fedora documents that the automatic-update component is not installed by default; after installation, its default policy downloads without installing. The documented setup uses &lt;code&gt;dnf5-plugin-automatic&lt;/code&gt; and an explicitly enabled timer.&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;/etc/dnf/automatic.conf&lt;/code&gt;, the timer, and the restart/reboot policy. Desktop offline updates and image-based/Atomic systems are different update paths.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sources: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.debian.org/doc/manuals/debian-handbook/sect.regular-upgrades.en.html" rel="noopener noreferrer"&gt;Debian's upgrade documentation&lt;/a&gt;,&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/9/html/managing_software_with_the_dnf_tool/assembly_automating-software-updates-in-rhel-9_managing-software-with-the-dnf-tool" rel="noopener noreferrer"&gt;RHEL 9 automatic updates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.fedoraproject.org/en-US/quick-docs/autoupdates/" rel="noopener noreferrer"&gt;Fedora automatic updates&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the documented DNF-based setups, useful inspection commands include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl list-timers &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="s1"&gt;'dnf*'&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^[[:space:]]*(apply_updates|download_updates|reboot)[[:space:]]*='&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /etc/dnf/automatic.conf

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Defaults are often bad for critical systems such as monolithic databases. None of these host-local tools automatically knows which PostgreSQL node is the current leader or whether taking down another replica is safe. The HA maintenance policy still has to be designed for the full stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every VM had its own maintenance window
&lt;/h2&gt;

&lt;p&gt;The effective configuration enabled daily unattended upgrades:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/apt/apt.conf.d/20auto-upgrades
apt-config dump | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'APT::Periodic::(Update-Package-Lists|Unattended-Upgrade)'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The configuration file contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The packaged timer definition, captured on all seven hosts, contained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /usr/lib/systemd/system/apt-daily-upgrade.timer

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Daily apt upgrade and clean activities&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;apt-daily.timer&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nn"&gt;[Timer]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;OnCalendar&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;*-*-* 6:00&lt;/span&gt;
&lt;span class="py"&gt;RandomizedDelaySec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;60m&lt;/span&gt;
&lt;span class="py"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nn"&gt;[Install]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;timers.target&lt;/span&gt;
&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The guests used UTC. Each machine independently scheduled maintenance around 06:00, with up to a 60-minute randomized delay. &lt;code&gt;Persistent=true&lt;/code&gt; also permits catch-up after a missed timer activation. This is not a cluster-aware rolling&lt;br&gt;
maintenance policy.&lt;/p&gt;

&lt;p&gt;I checked the unattended-upgrades logs and dpkg logs on all seven VMs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;VM&lt;/th&gt;
&lt;th&gt;Unattended upgrades started, UTC&lt;/th&gt;
&lt;th&gt;zlib upgrade, UTC&lt;/th&gt;
&lt;th&gt;Observed effect on the owned workload service&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;etcd2&lt;/td&gt;
&lt;td&gt;06:09:38.423&lt;/td&gt;
&lt;td&gt;06:09:48&lt;/td&gt;
&lt;td&gt;No etcd restart recorded; process start remained November 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pg1&lt;/td&gt;
&lt;td&gt;06:18:58.776&lt;/td&gt;
&lt;td&gt;06:19:03&lt;/td&gt;
&lt;td&gt;Patroni stop at 06:19:04; first primary lost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pg3&lt;/td&gt;
&lt;td&gt;06:19:28.899&lt;/td&gt;
&lt;td&gt;06:19:44&lt;/td&gt;
&lt;td&gt;Patroni stop at 06:19:45; standby also taken down&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;etcd1&lt;/td&gt;
&lt;td&gt;06:24:02.614&lt;/td&gt;
&lt;td&gt;06:24:09&lt;/td&gt;
&lt;td&gt;No etcd restart recorded; process start remained November 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;client&lt;/td&gt;
&lt;td&gt;06:26:39.054&lt;/td&gt;
&lt;td&gt;06:26:44&lt;/td&gt;
&lt;td&gt;Host-metrics service in restart list; load had already failed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;etcd3&lt;/td&gt;
&lt;td&gt;06:35:02.180&lt;/td&gt;
&lt;td&gt;06:35:20&lt;/td&gt;
&lt;td&gt;No etcd restart recorded; process start remained November 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pg2&lt;/td&gt;
&lt;td&gt;06:38:01.956&lt;/td&gt;
&lt;td&gt;06:38:16&lt;/td&gt;
&lt;td&gt;Patroni stop at 06:38:17; replacement primary lost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All seven listed the same set of packages for upgrade. Every PostgreSQL node's dpkg-output log explicitly included &lt;code&gt;franckpachot-lab-patroni.service&lt;/code&gt; in the immediate service-restart list.&lt;/p&gt;

&lt;p&gt;"Around the same time" means the same maintenance hour, not simultaneous restarts. pg1 and pg3 stopped only 41 seconds apart. pg2 stopped 19 minutes and 13 seconds after pg1, by which point it had become the primary.&lt;/p&gt;

&lt;p&gt;Random jitter spread the jobs out. It did not prevent maintenance from overlapping recovery or targeting the new leader.&lt;/p&gt;
&lt;h2&gt;
  
  
  Patroni and PostgreSQL logs show two failovers
&lt;/h2&gt;

&lt;p&gt;Run this on each PostgreSQL VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;--utc&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; short-iso &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s1"&gt;'2026-11-04 06:18:50 UTC'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--until&lt;/span&gt; &lt;span class="s1"&gt;'2026-11-04 06:45:00 UTC'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-u&lt;/span&gt; franckpachot-lab-patroni.service |
  &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'Stopping |Stopped |Started |promoted self|selected new timeline'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On pg2, the first promotion is explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-11-04T06:19:29+00:00 franckpachot-lab-pg2-kc-20261103 patroni[47953]: 2026-11-04 06:19:29,047 INFO: promoted self to leader by acquiring session lock
2026-11-04T06:19:29+00:00 franckpachot-lab-pg2-kc-20261103 patroni[48105]: 2026-11-04 06:19:29 UTC-6aea538b.bbe9-LOG:  selected new timeline ID: 23

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;pg3 was then stopped by its own maintenance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-11-04T06:19:45+00:00 franckpachot-lab-pg3-kc-20261103 systemd[1]: Stopping franckpachot-lab-patroni.service - franckpachot-lab PostgreSQL Patroni quorum lab...
2026-11-04T06:21:15+00:00 franckpachot-lab-pg3-kc-20261103 systemd[1]: Started franckpachot-lab-patroni.service - franckpachot-lab PostgreSQL Patroni quorum lab.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later, maintenance caught up with pg2, now the primary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-11-04T06:38:17+00:00 franckpachot-lab-pg2-kc-20261103 systemd[1]: Stopping franckpachot-lab-patroni.service - franckpachot-lab PostgreSQL Patroni quorum lab...

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two seconds later, pg3 promoted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-11-04T06:38:19+00:00 franckpachot-lab-pg3-kc-20261103 patroni[101738]: 2026-11-04 06:38:19,889 INFO: promoted self to leader by acquiring session lock
2026-11-04T06:38:19+00:00 franckpachot-lab-pg3-kc-20261103 patroni[101843]: 2026-11-04 06:38:19 UTC-6aeacedd.18dd3-LOG:  selected new timeline ID: 24

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting leadership sequence was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pg1 / timeline 22
  -&amp;gt; pg2 / timeline 23 at 06:19:29
  -&amp;gt; pg3 / timeline 24 at 06:38:19
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Patroni uses the DCS leader lock to coordinate leadership. Stopping a leader can release the lock or expire the lease, allowing an eligible replica to acquire leadership. These logs prove the promotions and new timelines. They do not by themselves establish whether a particular lock was explicitly deleted or expired after &lt;code&gt;ttl&lt;/code&gt;, as that would require additional&lt;br&gt;
lease/key history.&lt;/p&gt;

&lt;p&gt;For planned PostgreSQL restarts, Patroni provides &lt;code&gt;patronictl restart&lt;/code&gt; and the REST &lt;code&gt;/restart&lt;/code&gt; endpoint. Restarting the Patroni systemd service is not the same as a restart. A controlled maintenance plan may also require a deliberate switchover, replica checks, and application reconnection handling. HA does not make existing client sessions survive a primary shutdown.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why "automatic reboot disabled" did not protect me
&lt;/h2&gt;

&lt;p&gt;I initially looked at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep &lt;/span&gt;Reboot /etc/apt/apt.conf.d/50unattended-upgrades

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//Unattended-Upgrade::Automatic-Reboot "false";
// when Unattended-Upgrade::Automatic-Reboot is set to true
//Unattended-Upgrade::Automatic-Reboot-WithUsers "true";
//Unattended-Upgrade::Automatic-Reboot-Time "02:00";

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are commented examples/default documentation, not active assignments. Use &lt;code&gt;apt-config dump&lt;/code&gt; to inspect effective overrides rather than treating a comment as the final configuration.&lt;/p&gt;

&lt;p&gt;More importantly, a service restart is not a VM reboot. That option controls unattended-upgrades' automatic reboot behavior, not &lt;code&gt;needrestart&lt;/code&gt;'s service-restart behavior.&lt;/p&gt;

&lt;p&gt;To check the distinction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;uptime&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt;
journalctl &lt;span class="nt"&gt;--list-boots&lt;/span&gt; &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
last &lt;span class="nt"&gt;-x&lt;/span&gt; reboot | &lt;span class="nb"&gt;head&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The captured pg1 boot time remained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-11-03 16:24:54

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PostgreSQL VMs' boot times all preceded the incident. The observed 06:19 event was a service shutdown/restart, not a fresh guest boot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Durability is not availability
&lt;/h2&gt;

&lt;p&gt;Even with healthy replicas, sufficient retained/archived WAL, and successful node rejoin, these unplanned failovers would still be an availability incident. Recovering the data does not recover the application sessions that were connected to the stopped primary.&lt;/p&gt;

&lt;p&gt;It helps to separate three questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Durability / RPO:&lt;/strong&gt; which acknowledged transactions survive?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability / RTO:&lt;/strong&gt; when can the application successfully execute work again?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session and transaction continuity:&lt;/strong&gt; what happens to connections, in-flight transactions, and long-running operations?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WAL archiving is important for recoverability, but it does not preserve TCP connections or, by itself, guarantee zero data loss. The failover RPO depends on replication, commit settings, and which replica can safely be promoted. The point here is not a claim about lost committed data: even a zero-data-loss&lt;br&gt;
failover can interrupt every session connected to the old primary.&lt;/p&gt;

&lt;p&gt;A stable endpoint, virtual IP, or load balancer can direct new connections to the new leader. It cannot move an existing PostgreSQL session, its transaction, and its session state to another server. A proxy may keep a frontend socket open, but that is not a general guarantee that the backend transaction survives.&lt;/p&gt;

&lt;p&gt;The logs show about 25 seconds between pg1's shutdown request and pg2's promotion, and about two seconds between pg2's shutdown and pg3's promotion. Those are server event intervals, not measured application downtime. Endpoint health checks, connection timeouts, pool recovery, retries, and transaction replay can extend the interruption.&lt;/p&gt;

&lt;p&gt;For an application whose database sessions are terminated, that interruption is downtime, no matter how quickly a dashboard turns green again.&lt;/p&gt;
&lt;h2&gt;
  
  
  Resilience includes controlling avoidable failures
&lt;/h2&gt;

&lt;p&gt;Three availability zones protect against some infrastructure failures. They do not isolate nodes from the same package update and the same host-local restart policy.&lt;/p&gt;

&lt;p&gt;This was a correlated operational failure: the same maintenance mechanism ran independently on every VM, with no cluster-level gate. Random delay is not coordination. The second restart did not know that its target had just become the replacement primary.&lt;/p&gt;

&lt;p&gt;Automatic failover remains valuable for actual failures. But it is a recovery mechanism, not permission to turn routine maintenance into repeated unplanned outages. The objective should be to avoid unnecessary failovers, not merely celebrate that promotion worked twice.&lt;/p&gt;

&lt;p&gt;For resilience, I want both sides of the contract:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cluster-aware maintenance.&lt;/strong&gt; Patch replicas in a controlled sequence, verify health and catch-up before proceeding, preserve enough capacity for the configured synchronous-replication policy, and coordinate any leader transition. Apply equivalent quorum-aware sequencing to etcd. One host at a time is a starting point, not proof that every quorum configuration can tolerate that host being unavailable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application-aware recovery.&lt;/strong&gt; Exercise connection-pool recovery and bounded retries with backoff. Retry the right transaction or resumable unit of work, not an arbitrary fragment of a transaction. Handle uncertain commit outcomes with idempotency or reconciliation rather than assuming that a lost connection means nothing committed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application-level measurement.&lt;/strong&gt; Test successful requests, error rates, latency, and time to resume useful work, not just leader election. Include maintenance scenarios where a replica is already unavailable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An explicit disruption budget.&lt;/strong&gt; Define how many nodes may be unavailable and which checks must pass before continuing maintenance. Abort the rollout if recovery is incomplete rather than letting another timer fire.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A planned switchover can still disconnect clients. "Planned" does not mean "invisible." It makes the timing, sequencing, application handling, and expected impact deliberate. That is materially different from two independent package jobs unexpectedly stopping successive primaries.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I changed to avoid the issue happening again
&lt;/h2&gt;

&lt;p&gt;For this lab, I chose explicit maintenance windows rather&lt;br&gt;
than independent automatic updates on seven machines.&lt;/p&gt;

&lt;p&gt;First, I checked for active package transactions. I did not kill a running dpkg operation. Then I stopped, disabled, and persistently masked the named automatic-update units on all seven VMs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl disable &lt;span class="nt"&gt;--now&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  apt-daily.timer apt-daily-upgrade.timer unattended-upgrades.service
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl mask &lt;span class="se"&gt;\&lt;/span&gt;
  apt-daily.timer apt-daily-upgrade.timer unattended-upgrades.service

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also set &lt;code&gt;needrestart&lt;/code&gt; to list-only mode, so a later normal APT transaction would report affected services rather than automatically restarting them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s1"&gt;'$nrconf{restart} = "l";'&lt;/span&gt; |
  &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/needrestart/conf.d/zz-franckpachot-lab-list-only.conf

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The earlier targeted exclusions for the owned database, DCS, and supporting services remain in place. The broader list-only setting was an explicit subsequent decision, not the initial policy.&lt;/p&gt;

&lt;p&gt;The rollout verified each host's effective configuration, not just the existence of a new file. Every host reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VERIFIED: effective needrestart restart mode=l (list only)
apt-daily.timer enabled=masked active=inactive
apt-daily-upgrade.timer enabled=masked active=inactive
unattended-upgrades.service enabled=masked active=inactive

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To inspect unit state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl show apt-daily.timer apt-daily-upgrade.timer unattended-upgrades.service &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; Id &lt;span class="nt"&gt;-p&lt;/span&gt; UnitFileState &lt;span class="nt"&gt;-p&lt;/span&gt; ActiveState

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The effective &lt;code&gt;needrestart&lt;/code&gt; configuration was verified by loading its main Perl configuration, which includes the sorted drop-ins, and checking that &lt;code&gt;$nrconf{restart}&lt;/code&gt; resolves to &lt;code&gt;l&lt;/code&gt;. On the installed version, an explicit restart-mode setting is honored by the APT hook's &lt;code&gt;-m u&lt;/code&gt; mode.&lt;/p&gt;

&lt;p&gt;I also persisted the policy in provisioning and added a pre-operation check across all seven VMs. It refuses to proceed if maintenance protection cannot be verified. Fixing running machines without fixing their replacement images would only postpone the same surprise.&lt;/p&gt;

&lt;p&gt;This is not a recommendation to abandon security updates. It moves the responsibility to an explicit maintenance procedure:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Agree on a maintenance window and the acceptable application interruption.&lt;/li&gt;
&lt;li&gt;Verify replication, DCS health, and recoverability.&lt;/li&gt;
&lt;li&gt;Patch in a controlled sequence, coordinating service restarts and any necessary switchover through Patroni.&lt;/li&gt;
&lt;li&gt;Verify the cluster and apply the restarts needed to use updated libraries.&lt;/li&gt;
&lt;li&gt;Reverify maintenance protection and application readiness afterward.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The masks block these named systemd activation paths. List-only mode blocks &lt;code&gt;needrestart&lt;/code&gt;'s default automatic restart path. Neither is a universal barrier against explicit mode overrides, package maintainer scripts, manual restarts, VM reboots, or separately configured Azure patch orchestration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;The failure domain was not just a VM, a disk, or an availability zone. It was also the same maintenance policy installed independently on every node.&lt;/p&gt;

&lt;p&gt;Ubuntu was applying security fixes as designed. Patroni performed failovers when primaries stopped. The missing piece was coordination: routine maintenance should have been a planned operation, not a sequence of independently triggered primary failures.&lt;/p&gt;

&lt;p&gt;Durable data is essential, but it is not the whole HA contract. If every connection to the primary is dropped and applications must recover their work, availability has been affected. Resilience means reducing that impact, preventing avoidable disruptions, and electing a new leader.&lt;/p&gt;

&lt;p&gt;A database service spans layers that make decisions independently. In this case, Ubuntu restarted a service after a package upgrade, and Patroni responded to the resulting primary shutdown with a failover. Each did what it was configured to do, but the disruption stemmed from the lack of coordination between them. Kubernetes-based deployments can encounter similar interactions between controllers, as I explored in an earlier &lt;a href="https://dev.to/franckpachot/cloudnativepg-install-218-and-first-test-transient-failure-4ml"&gt;CloudNativePG transient-failure test&lt;/a&gt;. A managed service may use some of the same components, but the key difference is whether maintenance, failover, and client recovery are designed and tested together. PostgreSQL has no built-in high availability, but a managed service as a whole can provide it.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>ha</category>
      <category>patroni</category>
      <category>ubuntu</category>
    </item>
    <item>
      <title>PostgreSQL document compression: TOAST thresholds, storage policies, and page size</title>
      <dc:creator>Franck Pachot</dc:creator>
      <pubDate>Mon, 02 Nov 2026 21:14:00 +0000</pubDate>
      <link>https://dev.to/franckpachot/postgresql-document-compression-toast-thresholds-storage-policies-and-page-size-oh4</link>
      <guid>https://dev.to/franckpachot/postgresql-document-compression-toast-thresholds-storage-policies-and-page-size-oh4</guid>
      <description>&lt;p&gt;A common way to store documents in PostgreSQL is a table with an ID and a JSONB column. The open-source &lt;a href="https://documentdb.io/" rel="noopener noreferrer"&gt;DocumentDB&lt;/a&gt; extension instead stores BSON, the binary document format used by MongoDB, to provide more data types and MongoDB compatibility.&lt;/p&gt;

&lt;p&gt;Both approaches commonly combine two patterns that do not perfectly fit what SQL databases were designed for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rows with a single variable-length column, whose values can range from a few dozen bytes to many megabytes.&lt;/li&gt;
&lt;li&gt;Repeated data or metadata, such as similar field names and arrays of objects with the same schema, like time-series buckets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MongoDB's storage engine, WiredTiger, packs many documents into B-tree leaf pages. The default maximum page sizes — 32 KB for leaf pages and 4 KB for internal pages — are limits on the uncompressed page image fed to the block compressor, not on individual documents: MongoDB configures leaf_value_max=64MB, so a document larger than 32 KB simply produces a larger leaf page rather than overflow storage. Snappy compresses each page image for ordinary collections (time-series collections use Zstd, which ignores the global compressor setting), and the compressed result is written in multiples of the 4 KB allocation_size. Because compression runs over the whole page image, several small documents — including their repeated field names — contribute redundant byte sequences to the same compression input.&lt;/p&gt;

&lt;p&gt;A JSON or BSON document can contain substantial byte-sequence redundancy: repeated field names, common name prefixes, and repeated values or structures. LZ77-family compressors such as &lt;a href="https://github.com/google/snappy/blob/1.2.2/format_description.txt" rel="noopener noreferrer"&gt;Snappy&lt;/a&gt; and &lt;a href="https://github.com/lz4/lz4/blob/v1.10.0/doc/lz4_Block_format.md" rel="noopener noreferrer"&gt;LZ4&lt;/a&gt; can replace matching sequences with backreferences to earlier bytes. They don't need to understand JSON, and matches aren't restricted to the beginning of a document.&lt;/p&gt;

&lt;p&gt;Binary does not mean compressed: BSON includes field names, and repeated embedded objects repeat those names. JSONB can contain similar redundancy. PostgreSQL can compress those sequences in JSON, JSONB, or a BSON extension type. However, the decision to attempt compression is not driven by document redundancy. It belongs to TOAST's generic heap-row storage policy: reduce a wide row, trying column compression before storing permitted attributes externally in chunks.&lt;/p&gt;

&lt;p&gt;This distinction matters for small documents. It also makes increasing the page size potentially counterproductive: the same document can stop being compressed on a larger-page build.&lt;/p&gt;

&lt;p&gt;I will reproduce both effects, then compare the SQL controls with the hardcoded values. The examples use PostgreSQL 19 and core JSONB, so they can run without an extension or a custom patch. The distinction between whole-row activation and individual-column compression also applies to the TOAST-backed BSON values stored by DocumentDB.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is compressed, and what is measured?
&lt;/h2&gt;

&lt;p&gt;TOAST measures the whole tuple (row) to decide whether to start. However, compression acts on individual eligible column values, not on the entire row or all the documents in a page.&lt;/p&gt;

&lt;p&gt;PostgreSQL's ordinary table storage uses fixed-size pages, 8 KB by default. A row cannot span those pages. Large column values can instead be moved to an associated TOAST table, and the main row then contains a pointer to access the chunks in the TOAST table by index scan.&lt;/p&gt;

&lt;p&gt;JSONB still contains compressible byte sequences. It is a binary representation, not an already-compressed JSON string. Within one object, it normalizes key order and duplicate keys, but an array of similarly structured objects can repeat the same field names. No shared key dictionary exists across those objects or across independently stored JSONB rows.&lt;/p&gt;

&lt;p&gt;The distinction between repeated names within a document and repeated names across documents is important. PostgreSQL compresses one column value at a time. PostgreSQL calls each value a datum in its source code. That per-datum compression can use the former repetition, not the latter.&lt;/p&gt;

&lt;p&gt;MongoDB's WiredTiger storage engine uses Snappy block compression on disk by default for ordinary collections. Small documents can share a page, so one block-compression input can include repeated names and structures from several documents. This is not a separate compression call per small document. Other codecs can be configured, and time-series collections have different defaults. This is a difference in compression scope, not a capability reserved to Snappy.&lt;/p&gt;

&lt;p&gt;This post measures stored datum representations, not table allocation or query performance. I use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pg_column_size(doc::text::jsonb)&lt;/code&gt; to reconstruct an uncompressed logical JSONB datum for these ordinary JSON inputs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pg_column_compression(doc)&lt;/code&gt; to report the actual accepted method&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pg_column_size(doc)&lt;/code&gt; for its stored representation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pg_column_toast_chunk_id(doc) IS NOT NULL&lt;/code&gt; to identify external values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last function is available in PostgreSQL 19. For an external value, &lt;code&gt;pg_column_size&lt;/code&gt; is not the inline pointer size. The on-disk pointer in the main row occupies 18 bytes. The external size excludes the outer four-byte varlena length word, which explains some four-byte differences below without attributing them to the codec.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the environment and output
&lt;/h2&gt;

&lt;p&gt;All SQL needed for the experiments is included below. Run the statements in order in a fresh test database. For each page-size comparison, start a separate PostgreSQL instance.&lt;/p&gt;

&lt;p&gt;I use aligned ASCII output, with NULL displayed as &lt;code&gt;(null)&lt;/code&gt;. To reproduce that format, start an existing server's session with these options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="nt"&gt;-X&lt;/span&gt; &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nv"&gt;pager&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;off &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="s1"&gt;'null=(null)'&lt;/span&gt; &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nv"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;aligned &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nv"&gt;linestyle&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ascii &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nv"&gt;border&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-d&lt;/span&gt; postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are the initial settings on the source-built 8 KB server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;server_version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

 &lt;span class="n"&gt;server_version&lt;/span&gt; 
&lt;span class="c1"&gt;----------------&lt;/span&gt;
 &lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="n"&gt;rc1&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;block_size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

 &lt;span class="n"&gt;block_size&lt;/span&gt; 
&lt;span class="c1"&gt;------------&lt;/span&gt;
 &lt;span class="mi"&gt;8192&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;default_toast_compression&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

 &lt;span class="n"&gt;default_toast_compression&lt;/span&gt; 
&lt;span class="c1"&gt;---------------------------&lt;/span&gt;
 &lt;span class="n"&gt;lz4&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The selected codec is explicit in every test. PostgreSQL 14-18's upstream default is pglz, but an LZ4-enabled PostgreSQL 19 RC1 build defaults to LZ4. &lt;code&gt;SET COMPRESSION&lt;/code&gt; has been available since PostgreSQL 14.&lt;/p&gt;

&lt;p&gt;LZ4 is a sensible starting choice when compression/decompression CPU matters, but the workload still determines the trade-off. It does not always produce the smallest result. DocumentDB's current &lt;a href="https://github.com/documentdb/documentdb/blob/v1.0-0/packaging/postgresql-tools/documentdb.conf.sample#L57" rel="noopener noreferrer"&gt;setup/tuning configuration&lt;/a&gt; selects &lt;code&gt;default_toast_compression = 'lz4'&lt;/code&gt; when supported. This is a launcher-chosen PostgreSQL setting, not a property of BSON, nor a guarantee that every installation is configured that way. Check the live server and column settings rather than assuming them from a product name.&lt;/p&gt;

&lt;h2&gt;
  
  
  A compressible document below the activation threshold
&lt;/h2&gt;

&lt;p&gt;I construct a JSONB array containing eight objects with repeated &lt;code&gt;temperature&lt;/code&gt; and &lt;code&gt;humidity&lt;/code&gt; names. Both rows receive the same document. The only difference is an unrelated column stored &lt;code&gt;PLAIN&lt;/code&gt;, so it cannot be compressed or moved externally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;small_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;small_documents&lt;/span&gt;
            &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;COMPRESSION&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;small_documents&lt;/span&gt;
            &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;STORAGE&lt;/span&gt; &lt;span class="n"&gt;PLAIN&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;small_documents&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'samples'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jsonb_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'temperature'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'humidity'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
             &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="p"&gt;)),&lt;/span&gt;
           &lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'q'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;1600&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;pg_column_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;logical_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;octet_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;other_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;pg_column_compression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;compression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;pg_column_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;stored_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;pg_column_toast_chunk_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;external&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;small_documents&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

 &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;logical_bytes&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;other_bytes&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;compression&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;stored_bytes&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;external&lt;/span&gt; 
&lt;span class="c1"&gt;----+---------------+-------------+-------------+--------------+----------&lt;/span&gt;
  &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;        &lt;span class="mi"&gt;1600&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;109&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first 508-byte JSONB datum remains uncompressed. The second compresses to 109 bytes, even though its uncompressed document bytes are identical. The additional 1,600-byte column made the tuple large enough to enter TOAST processing.&lt;/p&gt;

&lt;p&gt;This proves both points:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Repeated field names and values in JSONB are compressible.&lt;/li&gt;
&lt;li&gt;PostgreSQL does not independently ask whether that small JSONB datum would benefit from compression. The enclosing tuple controls activation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I used an unrelated column for academic purposes. Of course, I'm not suggesting that adding more data to trigger compression is a good idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hardcoded gate is not the per-table target
&lt;/h2&gt;

&lt;p&gt;In the &lt;a href="https://github.com/postgres/postgres/blob/REL_19_RC1/src/include/access/heaptoast.h" rel="noopener noreferrer"&gt;PostgreSQL source&lt;/a&gt;, the ordinary values are derived as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#define TOAST_TUPLES_PER_PAGE 4
#define TOAST_TUPLE_THRESHOLD MaximumBytesPerTuple(TOAST_TUPLES_PER_PAGE)
#define TOAST_TUPLE_TARGET TOAST_TUPLE_THRESHOLD
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MaximumBytesPerTuple(4)&lt;/code&gt; accounts for page headers, line pointers, and alignment. It is not exactly &lt;code&gt;block_size / 4&lt;/code&gt;. Here are the values for the default 8 KB page size and for PostgreSQL builds with 16 KB or 32 KB pages:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PostgreSQL page size&lt;/th&gt;
&lt;th&gt;Fixed tuple activation threshold&lt;/th&gt;
&lt;th&gt;Default tuple target&lt;/th&gt;
&lt;th&gt;Maximum native TOAST chunk payload&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8,192 bytes&lt;/td&gt;
&lt;td&gt;2,032 bytes&lt;/td&gt;
&lt;td&gt;2,032 bytes&lt;/td&gt;
&lt;td&gt;1,996 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16,384 bytes&lt;/td&gt;
&lt;td&gt;4,080 bytes&lt;/td&gt;
&lt;td&gt;4,080 bytes&lt;/td&gt;
&lt;td&gt;4,044 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32,768 bytes&lt;/td&gt;
&lt;td&gt;8,176 bytes&lt;/td&gt;
&lt;td&gt;8,176 bytes&lt;/td&gt;
&lt;td&gt;8,140 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are the values for the tested eight-byte-aligned builds. The chunk capacities were also observed from actual external data, not inferred from page size alone.&lt;/p&gt;

&lt;p&gt;Both &lt;a href="https://github.com/postgres/postgres/blob/REL_19_RC1/src/backend/access/heap/heapam.c#L2266" rel="noopener noreferrer"&gt;&lt;code&gt;heap_prepare_insert&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/postgres/postgres/blob/REL_19_RC1/src/backend/access/heap/heapam.c#L3842" rel="noopener noreferrer"&gt;&lt;code&gt;heap_update&lt;/code&gt;&lt;/a&gt; check the fixed threshold before invoking the toaster for fresh inline tuples. Inside the toaster, &lt;code&gt;toast_tuple_target&lt;/code&gt; provides the reduction target, after subtracting tuple header overhead.&lt;/p&gt;

&lt;p&gt;It’s not simply "&lt;em&gt;compress when the row cannot fit on a page&lt;/em&gt;.” A tuple just over 2 KB can still fit on an 8 KB page. By default, wide rows are reduced earlier, with the goal of keeping their size consistent with roughly four tuples per page. This is a row-density and oversized-attribute policy, rather than a separately tunable document-compression policy.&lt;/p&gt;

&lt;p&gt;These are fresh values with one eligible document column. If a row has several eligible columns, the toaster works in phases rather than compressing the entire row as one object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change the target without changing activation
&lt;/h2&gt;

&lt;p&gt;The following input view constructs fresh JSONB values from repetitive text. It deliberately samples several payload sizes. It is not intended to represent a natural document distribution.&lt;/p&gt;

&lt;p&gt;A view is used rather than copying stored compressed values from another table. Reusing an existing compressed datum would confound a test of whether the destination attempts compression.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TEMP&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;input_documents&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'v'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;extended_default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;extended_200&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toast_tuple_target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;extended_4k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toast_tuple_target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;extended_default&lt;/span&gt;
            &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;COMPRESSION&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;extended_200&lt;/span&gt;
            &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;COMPRESSION&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;extended_4k&lt;/span&gt;
            &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;COMPRESSION&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;extended_default&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;input_documents&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;extended_200&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;input_documents&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;extended_4k&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;input_documents&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;payload_kib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;pg_column_compression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;compression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;pg_column_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;stored_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;pg_column_toast_chunk_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;external&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;extended_default&lt;/span&gt;
      &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'target_200'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;extended_200&lt;/span&gt;
      &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'target_4096'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;extended_4k&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="n"&gt;policy&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;payload_kib&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;compression&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;stored_bytes&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;external&lt;/span&gt; 
&lt;span class="c1"&gt;-------------+-------------+-------------+--------------+----------&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;1041&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;39&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;47&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;288&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;512&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;2083&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;        &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;4139&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;        &lt;span class="mi"&gt;2048&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;8252&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
 &lt;span class="n"&gt;target_200&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;1041&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_200&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;39&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_200&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;47&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_200&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;284&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
 &lt;span class="n"&gt;target_200&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;512&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;2083&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
 &lt;span class="n"&gt;target_200&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt;        &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;4139&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
 &lt;span class="n"&gt;target_200&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt;        &lt;span class="mi"&gt;2048&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;8252&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
 &lt;span class="n"&gt;target_4096&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;1041&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_4096&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;2065&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_4096&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;47&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_4096&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;288&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_4096&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;512&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;2087&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_4096&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;        &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;4139&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
 &lt;span class="n"&gt;target_4096&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;        &lt;span class="mi"&gt;2048&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;8252&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default JSONB storage strategy is &lt;code&gt;EXTENDED&lt;/code&gt;, which permits both compression and external storage.&lt;/p&gt;

&lt;p&gt;Three observations distinguish the decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Even with a 200-byte target, the 1 KB-payload document still remains uncompressed and inline at 1,041 bytes. Its tuple did not enter TOAST because it is smaller than the hardcoded &lt;code&gt;MaximumBytesPerTuple(4)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Once TOAST is active, lowering the target externalizes values sooner: the 64 KB payload is inline with the default target but external with the 200-byte target.&lt;/li&gt;
&lt;li&gt;With a 4 KB target, the 2 KB-payload document remains uncompressed at 2,065 bytes. Its tuple enters TOAST, but already meets the larger target. The same document compressed to 39 bytes in the other two tables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Raising the target is therefore not an independent "&lt;em&gt;retain more compressed documents inline&lt;/em&gt;" control. It can also prevent a compression attempt.&lt;/p&gt;

&lt;p&gt;The default and 200-byte cases report 288 and 284 bytes respectively for the same compressed 64 KB-payload document. This is the inline/external varlena accounting difference, not a better compression result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage strategies: what SQL can control
&lt;/h2&gt;

&lt;p&gt;There are four column storage strategies:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Compression&lt;/th&gt;
&lt;th&gt;External storage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EXTENDED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Permitted, attempted in the first phase when reduction is needed&lt;/td&gt;
&lt;td&gt;Permitted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EXTERNAL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not permitted&lt;/td&gt;
&lt;td&gt;Permitted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MAIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Permitted, attempted in a later phase when needed&lt;/td&gt;
&lt;td&gt;Last resort&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PLAIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not permitted&lt;/td&gt;
&lt;td&gt;Not permitted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;MAIN&lt;/code&gt; does not guarantee inline storage. MAIN attributes are also skipped during the first compression pass: they get their own compression round only after EXTENDED/EXTERNAL attributes have been compressed and externalized. They are likewise skipped by the out-of-line loops until the last-resort pass, which uses TOAST_TUPLE_TARGET_MAIN (&lt;code&gt;MaximumBytesPerTuple(1)&lt;/code&gt; ≈ 8,160 bytes on an 8 KB build). &lt;code&gt;EXTERNAL&lt;/code&gt; does not mean "externalize every document,” and an explicit LZ4 method cannot override its prohibition on compression.&lt;/p&gt;

&lt;p&gt;Here &lt;code&gt;EXTERNAL&lt;/code&gt; and &lt;code&gt;MAIN&lt;/code&gt; use the same 4 KB target, making the comparison independent of an accidental target difference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;external_4k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toast_tuple_target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;main_4k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toast_tuple_target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;plain_case&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;external_4k&lt;/span&gt;
            &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;COMPRESSION&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;main_4k&lt;/span&gt;
            &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;COMPRESSION&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;plain_case&lt;/span&gt;
            &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;COMPRESSION&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;external_4k&lt;/span&gt;
            &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;STORAGE&lt;/span&gt; &lt;span class="k"&gt;EXTERNAL&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;main_4k&lt;/span&gt;
            &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;STORAGE&lt;/span&gt; &lt;span class="n"&gt;MAIN&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;plain_case&lt;/span&gt;
            &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;STORAGE&lt;/span&gt; &lt;span class="n"&gt;PLAIN&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;external_4k&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;input_documents&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;main_4k&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;input_documents&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;plain_case&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;input_documents&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;payload_kib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;pg_column_compression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;compression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;pg_column_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;stored_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;pg_column_toast_chunk_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;external&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'EXTERNAL'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;external_4k&lt;/span&gt;
      &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'MAIN'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;main_4k&lt;/span&gt;
      &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'PLAIN'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;plain_case&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;
    &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;policy&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;payload_kib&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;compression&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;stored_bytes&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;external&lt;/span&gt; 
&lt;span class="c1"&gt;----------+-------------+-------------+--------------+----------&lt;/span&gt;
 &lt;span class="k"&gt;EXTERNAL&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;1041&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="k"&gt;EXTERNAL&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;2065&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="k"&gt;EXTERNAL&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;4109&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
 &lt;span class="k"&gt;EXTERNAL&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;        &lt;span class="mi"&gt;65549&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
 &lt;span class="k"&gt;EXTERNAL&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;512&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;524301&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
 &lt;span class="k"&gt;EXTERNAL&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;        &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;1048589&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
 &lt;span class="k"&gt;EXTERNAL&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;        &lt;span class="mi"&gt;2048&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;2097165&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
 &lt;span class="n"&gt;MAIN&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;1041&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;MAIN&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;2065&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;MAIN&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;47&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;MAIN&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;288&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;MAIN&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;512&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;2087&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;MAIN&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;        &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;4143&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;MAIN&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;        &lt;span class="mi"&gt;2048&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;8252&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
 &lt;span class="n"&gt;PLAIN&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;1041&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;PLAIN&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;2065&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;PLAIN&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;4113&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For &lt;code&gt;EXTERNAL&lt;/code&gt;, the 2 KB payload is still inline, but the 4 KB payload is external without compression.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;MAIN&lt;/code&gt;, the 1 MB payload compresses to 4,143 inline bytes. It can stay inline because &lt;code&gt;MAIN&lt;/code&gt; postpones externalization to its last-resort target: the maximum tuple size that fits on one page. The larger 2 MB-payload document still becomes external after compression.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;PLAIN&lt;/code&gt;, no value compresses or becomes external. A row that cannot fit on the heap page fails. Run this separate statement expecting an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;plain_case&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'v'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;8192&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="mi"&gt;54000&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;too&lt;/span&gt; &lt;span class="n"&gt;big&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;size&lt;/span&gt; &lt;span class="mi"&gt;8240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maximum&lt;/span&gt; &lt;span class="k"&gt;size&lt;/span&gt; &lt;span class="mi"&gt;8160&lt;/span&gt;
&lt;span class="k"&gt;LOCATION&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;RelationGetBufferForTuple&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;531&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 8,160-byte maximum in this error is the heap-page tuple limit, not the 2,032-byte TOAST activation threshold. They answer different questions.&lt;/p&gt;

&lt;p&gt;This is the complete SQL control surface relevant to these cases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Changes the fixed activation threshold?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ALTER COLUMN ... SET STORAGE ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Column's permitted/preferred representation&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ALTER COLUMN ... SET COMPRESSION pglz/lz4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Codec when compression is attempted&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;default_toast_compression&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Default codec when the column has no override&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ALTER TABLE ... SET (toast_tuple_target=...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whole-tuple reduction target after entry&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;TOAST_TUPLE_THRESHOLD&lt;/code&gt;, &lt;code&gt;TOAST_TUPLES_PER_PAGE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Compiled activation policy&lt;/td&gt;
&lt;td&gt;Source change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;TOAST_MAX_CHUNK_SIZE&lt;/code&gt;, &lt;code&gt;EXTERN_TUPLES_PER_PAGE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Compiled external chunk capacity&lt;/td&gt;
&lt;td&gt;Separate from activation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BLCKSZ&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Compiled page size&lt;/td&gt;
&lt;td&gt;Changes several defaults together&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Changing page and chunk sizes requires a freshly initialized cluster. They are not runtime GUCs. Changing only a main-tuple threshold is not inherently an on-disk page-format change, but a correct implementation still has to consider all heap entry paths and TOAST-table creation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The compression algorithm still matters after activation
&lt;/h2&gt;

&lt;p&gt;Compression is an attempt, not a storage guarantee. Pglz's default strategy has a 32-byte minimum input, a 25% minimum reduction policy, and a 1,024-byte encoded-output first-success limit: it aborts if it has emitted that much without finding a match.&lt;/p&gt;

&lt;p&gt;Those are &lt;a href="https://github.com/postgres/postgres/blob/REL_19_RC1/src/common/pg_lzcompress.c" rel="noopener noreferrer"&gt;pglz strategy rules&lt;/a&gt;, not user-configurable document thresholds. LZ4 does not impose pglz's first-success or minimum-rate rules. Both must still pass TOAST's &lt;a href="https://github.com/postgres/postgres/blob/REL_19_RC1/src/backend/access/common/toast_internals.c" rel="noopener noreferrer"&gt;header/alignment savings check&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Changing the column codec does not rewrite existing values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;codec_example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;codec_example&lt;/span&gt;
            &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;COMPRESSION&lt;/span&gt; &lt;span class="n"&gt;pglz&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;codec_example&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'v'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;codec_example&lt;/span&gt;
            &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;COMPRESSION&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;codec_example&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'v'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'x'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pg_column_compression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;compression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;pg_column_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;stored_bytes&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;codec_example&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

 &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;compression&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;stored_bytes&lt;/span&gt; 
&lt;span class="c1"&gt;----+-------------+--------------&lt;/span&gt;
  &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;pglz&lt;/span&gt;        &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;71&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;47&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old value remains pglz. The freshly constructed new value uses LZ4. An update that leaves the document unchanged, or a copy of an already compressed datum, need not recompress it. Therefore, changing the setting is not proof that existing data now uses the new codec.&lt;/p&gt;

&lt;h2&gt;
  
  
  Larger pages: fewer documents become compression candidates
&lt;/h2&gt;

&lt;p&gt;Larger pages can reduce the number of external chunks. That does not mean they preserve compression behavior.&lt;/p&gt;

&lt;p&gt;I rebuilt PostgreSQL 19 RC1 with &lt;code&gt;--with-blocksize=8&lt;/code&gt;, &lt;code&gt;16&lt;/code&gt;, and &lt;code&gt;32&lt;/code&gt;, keeping the other configure options the same, then initialized fresh clusters.&lt;/p&gt;

&lt;p&gt;Do not reuse a data directory across page sizes. A different page size also requires rebuilding linked extensions against that PostgreSQL installation. This comparison deliberately uses core JSONB rather than mixing extension versions or a stock image with a different source-built baseline.&lt;/p&gt;

&lt;h3&gt;
  
  
  8 KB: all but the smallest repeated-field document compress
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;samples&lt;/code&gt; array now contains 8, 64, 128, or 256 similarly structured objects. All tests explicitly select LZ4. One table retains the build's default target. The other sets it to 2040.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TEMP&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;page_inputs&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'samples'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jsonb_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'temperature'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'humidity'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
             &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;page_default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;page_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toast_tuple_target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2040&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;page_default&lt;/span&gt;
            &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;COMPRESSION&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;page_target&lt;/span&gt;
            &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;COMPRESSION&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;page_default&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;page_inputs&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;page_target&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;page_inputs&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;page_observations&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;pg_column_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;logical_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;pg_column_compression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;compression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;pg_column_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;stored_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;pg_column_toast_chunk_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;external&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'default'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;page_default&lt;/span&gt;
      &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'target_2040'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;page_target&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;page_observations&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="n"&gt;policy&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;objects&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;logical_bytes&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;compression&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;stored_bytes&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;external&lt;/span&gt; 
&lt;span class="c1"&gt;-------------+---------+---------------+-------------+--------------+----------&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;3868&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;126&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;     &lt;span class="mi"&gt;128&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;7708&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;150&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;     &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;15388&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;198&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_2040&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_2040&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;3868&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;126&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_2040&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;     &lt;span class="mi"&gt;128&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;7708&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;150&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_2040&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;     &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;15388&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;198&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  16 KB: the 3,868-byte JSONB document no longer compresses
&lt;/h3&gt;

&lt;p&gt;The complete setup is unchanged. This is the same final query on the 16 KB build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;page_observations&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="n"&gt;policy&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;objects&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;logical_bytes&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;compression&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;stored_bytes&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;external&lt;/span&gt; 
&lt;span class="c1"&gt;-------------+---------+---------------+-------------+--------------+----------&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;3868&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;3868&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;     &lt;span class="mi"&gt;128&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;7708&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;150&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;     &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;15388&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;198&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_2040&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_2040&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;3868&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;3868&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_2040&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;     &lt;span class="mi"&gt;128&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;7708&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;150&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_2040&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;     &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;15388&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;198&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tuple containing 3,868 JSONB bytes remains below the 4,080-byte activation gate. Lowering its target to 2040 does not make the caller invoke TOAST.&lt;/p&gt;

&lt;h3&gt;
  
  
  32 KB: the 7,708-byte document also stops compressing
&lt;/h3&gt;

&lt;p&gt;The same query on the 32 KB build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;page_observations&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="n"&gt;policy&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;objects&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;logical_bytes&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;compression&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;stored_bytes&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;external&lt;/span&gt; 
&lt;span class="c1"&gt;-------------+---------+---------------+-------------+--------------+----------&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;3868&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;3868&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;     &lt;span class="mi"&gt;128&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;7708&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;7708&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;     &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;15388&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;198&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_2040&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_2040&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;3868&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;3868&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_2040&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;     &lt;span class="mi"&gt;128&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;7708&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;7708&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_2040&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;     &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;15388&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;198&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 7,708-byte JSONB value compressed to 150 bytes on the 8- and 16-KB builds. On the 32 KB build, the tuple stays below the 8,176-byte gate, and the datum remains at 7,708 bytes. A 2040-byte target does not restore compression.&lt;/p&gt;

&lt;p&gt;The 15,388-byte positive control compresses on all three builds. Its tuple exceeds even the largest activation threshold.&lt;/p&gt;

&lt;h3&gt;
  
  
  UPDATE has the same outer gate
&lt;/h3&gt;

&lt;p&gt;The following statements reconstruct each JSONB value on UPDATE, rather than reassigning an existing compressed value. On the 32 KB build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;page_default&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'samples'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jsonb_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'temperature'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'humidity'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;page_target&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;
    &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'samples'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jsonb_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'temperature'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'humidity'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;page_observations&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="n"&gt;policy&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;objects&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;logical_bytes&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;compression&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;stored_bytes&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;external&lt;/span&gt; 
&lt;span class="c1"&gt;-------------+---------+---------------+-------------+--------------+----------&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;3868&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;3868&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;     &lt;span class="mi"&gt;128&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;7708&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;7708&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="k"&gt;default&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt;     &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;15388&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;198&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_2040&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;       &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;508&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_2040&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="mi"&gt;64&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;3868&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;3868&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_2040&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;     &lt;span class="mi"&gt;128&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;7708&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;7708&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
 &lt;span class="n"&gt;target_2040&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;     &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="mi"&gt;15388&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;lz4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt;          &lt;span class="mi"&gt;198&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The INSERT and UPDATE results matched on all three servers. This is why looking only at the inner compression loop, without the INSERT/UPDATE callers, can give the wrong interpretation of &lt;code&gt;toast_tuple_target&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chunk capacity does grow
&lt;/h3&gt;

&lt;p&gt;This 8 KB-page case inserts a 65,536-character &lt;code&gt;text&lt;/code&gt; value with &lt;code&gt;STORAGE EXTERNAL&lt;/code&gt; and reads its actual TOAST chunks. &lt;code&gt;\gset&lt;/code&gt; assigns the TOAST relation name to a psql variable, avoiding a hardcoded relation OID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;chunk_probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;chunk_probe&lt;/span&gt;
            &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;STORAGE&lt;/span&gt; &lt;span class="k"&gt;EXTERNAL&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;chunk_probe&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'z'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;reltoastrelid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;toast_relation&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'chunk_probe'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;
    &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;gset&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;octet_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_chunk_bytes&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;toast_relation&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

 &lt;span class="n"&gt;max_chunk_bytes&lt;/span&gt; 
&lt;span class="c1"&gt;-----------------&lt;/span&gt;
            &lt;span class="mi"&gt;1996&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the same statements in the 16 KB instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;chunk_probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;chunk_probe&lt;/span&gt;
            &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;STORAGE&lt;/span&gt; &lt;span class="k"&gt;EXTERNAL&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;chunk_probe&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'z'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;reltoastrelid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;toast_relation&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'chunk_probe'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;
    &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;gset&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;octet_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_chunk_bytes&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;toast_relation&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

 &lt;span class="n"&gt;max_chunk_bytes&lt;/span&gt; 
&lt;span class="c1"&gt;-----------------&lt;/span&gt;
            &lt;span class="mi"&gt;4044&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the 32 KB instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;chunk_probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;chunk_probe&lt;/span&gt;
            &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;STORAGE&lt;/span&gt; &lt;span class="k"&gt;EXTERNAL&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;chunk_probe&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'z'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;reltoastrelid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;toast_relation&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'chunk_probe'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;
    &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;gset&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;octet_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_chunk_bytes&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;toast_relation&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

 &lt;span class="n"&gt;max_chunk_bytes&lt;/span&gt; 
&lt;span class="c1"&gt;-----------------&lt;/span&gt;
            &lt;span class="mi"&gt;8140&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The maximum payloads are 1,996, 4,044, and 8,140 bytes.&lt;/p&gt;

&lt;p&gt;That is an independent benefit larger pages can provide for sufficiently large values. It does not compensate automatically for the loss of compression on smaller documents. Neither fewer chunks nor larger pages alone establishes lower allocation or better read performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this behavior remains
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.postgresql.org/docs/19/storage-toast.html" rel="noopener noreferrer"&gt;documented TOAST design&lt;/a&gt; handles values that cannot fit directly in heap tuples, and keeps main rows smaller so queries not needing large attributes can avoid fetching them. It is a general relational storage mechanism, predating JSONB, not a policy specifically optimized for repeated document schemas.&lt;/p&gt;

&lt;p&gt;I searched the pgsql-hackers discussions with the &lt;a href="https://pghacker-mcp.dev/" rel="noopener noreferrer"&gt;pghacker MCP&lt;/a&gt;. This separation was already being discussed explicitly in 2019:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;February 6, 2019: Pavan Deolasee proposed &lt;a href="https://www.postgresql.org/message-id/CABOikdP0N3HaMObZrbqyTg8-PotUxbeRRnQn0%3DVsXHhad6%3D56w%40mail.gmail.com" rel="noopener noreferrer"&gt;a separate table-level option to control compression&lt;/a&gt;, named &lt;code&gt;compress_tuple_target&lt;/code&gt;, so smaller values could be compressed inline without requiring out-of-line storage.&lt;/li&gt;
&lt;li&gt;March 5, 2019: Andrew Dunstan &lt;a href="https://www.postgresql.org/message-id/d7ac48ea-696a-88f9-f88e-9d63875ff75c%402ndQuadrant.com" rel="noopener noreferrer"&gt;supported the idea&lt;/a&gt;, while calling for more review.&lt;/li&gt;
&lt;li&gt;April 3, 2019: Michael Paquier identified &lt;a href="https://www.postgresql.org/message-id/20190403063759.GF3298%40paquier.xyz" rel="noopener noreferrer"&gt;the fixed-threshold caller checks&lt;/a&gt; independently of the per-table &lt;code&gt;toast_tuple_target&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://www.postgresql.org/message-id/20190514063941.GB1889%40paquier.xyz" rel="noopener noreferrer"&gt;subsequent discussion&lt;/a&gt; identified two broader problems: a bounded-width relation may have no TOAST table, so lowering its trigger could require creating one under a stronger lock. Rejecting previously accepted low targets could break dump restores. These are implementation and compatibility constraints, not evidence that the current fraction is optimal for small JSONB documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to build and run the three page sizes
&lt;/h2&gt;

&lt;p&gt;The SQL above is sufficient for an existing PostgreSQL 19 server. The following commands run in Bash on a machine with Docker. The Dockerfile is supplied inline. No repository checkout or external code file is required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;set -euo pipefail&lt;/span&gt;

&lt;span class="s"&gt;for kb in 8 16 32; do&lt;/span&gt;
  &lt;span class="s"&gt;docker build --build-arg BLOCK_SIZE="$kb" \&lt;/span&gt;
    &lt;span class="s"&gt;-t "toast-pg19rc1:${kb}k" - &amp;lt;&amp;lt;'DOCKERFILE'&lt;/span&gt;
&lt;span class="s"&gt;FROM postgres:19rc1-alpine&lt;/span&gt;
&lt;span class="s"&gt;RUN apk add --no-cache build-base curl bison flex perl linux-headers \&lt;/span&gt;
    &lt;span class="s"&gt;icu-dev lz4-dev openssl-dev readline-dev zlib-dev&lt;/span&gt;
&lt;span class="s"&gt;ARG BLOCK_SIZE=8&lt;/span&gt;
&lt;span class="s"&gt;RUN set -eu; \&lt;/span&gt;
    &lt;span class="s"&gt;case "$BLOCK_SIZE" in 8|16|32) ;; *) exit 1 ;; esac; \&lt;/span&gt;
    &lt;span class="s"&gt;mkdir /tmp/page-build; \&lt;/span&gt;
    &lt;span class="s"&gt;curl --fail --location \&lt;/span&gt;
      &lt;span class="s"&gt;"https://ftp.postgresql.org/pub/source/v${PG_VERSION}/postgresql-${PG_VERSION}.tar.bz2" \&lt;/span&gt;
      &lt;span class="s"&gt;-o /tmp/page-build/postgres.tar.bz2; \&lt;/span&gt;
    &lt;span class="s"&gt;tar -xjf /tmp/page-build/postgres.tar.bz2 -C /tmp/page-build; \&lt;/span&gt;
    &lt;span class="s"&gt;cd "/tmp/page-build/postgresql-${PG_VERSION}"; \&lt;/span&gt;
    &lt;span class="s"&gt;./configure --prefix=/usr/local --with-blocksize="$BLOCK_SIZE" \&lt;/span&gt;
      &lt;span class="s"&gt;--with-lz4 --with-icu --with-openssl; \&lt;/span&gt;
    &lt;span class="s"&gt;make -s -j2; \&lt;/span&gt;
    &lt;span class="s"&gt;make -s install; \&lt;/span&gt;
    &lt;span class="s"&gt;cd /; \&lt;/span&gt;
    &lt;span class="s"&gt;rm -rf /tmp/page-build&lt;/span&gt;
&lt;span class="s"&gt;DOCKERFILE&lt;/span&gt;
&lt;span class="s"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Choose one page size, start a fresh disposable instance, and open &lt;code&gt;psql&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;kb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8                      &lt;span class="c"&gt;# repeat with 16 and 32&lt;/span&gt;
&lt;span class="nv"&gt;pg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"toast-research-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;kb&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;k-&lt;/span&gt;&lt;span class="nv"&gt;$$&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
docker run &lt;span class="nt"&gt;--detach&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--network&lt;/span&gt; none &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_HOST_AUTH_METHOD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;trust &lt;span class="s2"&gt;"toast-pg19rc1:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;kb&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;k"&lt;/span&gt;

&lt;span class="nv"&gt;ready&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no
&lt;span class="k"&gt;for &lt;/span&gt;attempt &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 60&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if &lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; pg_isready &lt;span class="nt"&gt;-U&lt;/span&gt; postgres&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;ready&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes
    break
  &lt;/span&gt;&lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
&lt;span class="k"&gt;done
if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ready&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;docker logs &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  docker &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--volumes&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; psql &lt;span class="nt"&gt;-X&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;ON_ERROR_STOP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nv"&gt;pager&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;off &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="s1"&gt;'null=(null)'&lt;/span&gt; &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nv"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;aligned &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nv"&gt;linestyle&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ascii &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nv"&gt;border&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-d&lt;/span&gt; postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the 8 KB instance for the storage-strategy cases. For the page comparison, run the same &lt;code&gt;page_inputs&lt;/code&gt;, &lt;code&gt;page_default&lt;/code&gt;, &lt;code&gt;page_target&lt;/code&gt;, and observation statements on each instance. Repeat the chunk case on all three.&lt;/p&gt;

&lt;p&gt;The deliberately failing &lt;code&gt;PLAIN&lt;/code&gt; insert is a separate statement: do not include it in a successful setup batch with &lt;code&gt;ON_ERROR_STOP&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After leaving &lt;code&gt;psql&lt;/code&gt; with &lt;code&gt;\q&lt;/code&gt;, remove only the container created above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--volumes&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion: separate compression eligibility from row placement
&lt;/h2&gt;

&lt;p&gt;The experiment does not show that PostgreSQL cannot compress document redundancy. It shows that the eligibility policy is tied to heap tuple size and page size, rather than to a document-column compression policy.&lt;/p&gt;

&lt;p&gt;Under the ordinary defaults, increasing page size from 8 to 16 to 32 KB raises activation from about 2 to 4 to 8 KB. Some highly compressible documents therefore stop reaching the compressor. A lower &lt;code&gt;toast_tuple_target&lt;/code&gt; cannot recover those missed attempts. A higher target can suppress attempts further.&lt;/p&gt;

&lt;p&gt;A useful direction is to make the hardcoded activation policy configurable, separately from the reduction/inline target. For example, a 32 KB-page server could retain a lower activation threshold without adopting a smaller page. For document workloads, a per-column compression-eligibility threshold may be more directly useful than a whole-row threshold.&lt;/p&gt;

&lt;p&gt;This isn't as simple as making the hardcoded value a parameter. A safe implementation must cover INSERT, UPDATE, rewrites, TOAST-table creation, locking, and existing representations. The appropriate default would still need insertion CPU and read/update benchmarks on representative document sizes. There is no basis here for declaring that every 500-byte document should be compressed.&lt;/p&gt;

&lt;p&gt;The narrower conclusion is reproducible: the quarter-page activation gate can prevent useful compression, and larger pages may widen the gap. Currently, the &lt;a href="https://documentdb.io/" rel="noopener noreferrer"&gt;DocumentDB&lt;/a&gt; extension relies on the TOAST mechanism, trading MongoDB’s compression behavior for compatibility with PostgreSQL storage. I've seen many MongoDB workloads store small documents (and ran a &lt;a href="https://lnkd.in/p/etz-P6CT" rel="noopener noreferrer"&gt;poll&lt;/a&gt; on LinkedIn), much like a relational model—even though document models are meant to store larger aggregates to favor single-document data locality and single-IO transactions.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>document</category>
      <category>database</category>
      <category>toast</category>
    </item>
    <item>
      <title>Load CSV and JSON from Azure Blob into Postgres with pg_duckdb</title>
      <dc:creator>Franck Pachot</dc:creator>
      <pubDate>Fri, 23 Oct 2026 13:02:31 +0000</pubDate>
      <link>https://dev.to/franckpachot/load-csv-and-json-from-azure-blob-into-postgres-with-pgduckdb-1gjd</link>
      <guid>https://dev.to/franckpachot/load-csv-and-json-from-azure-blob-into-postgres-with-pgduckdb-1gjd</guid>
      <description>&lt;p&gt;Data arrives as files. A vendor drops a CSV in a container, an upstream system exports newline-delimited JSON, a daily extract lands in Blob. The usual way into Postgres is a client-side load tool and a hand-written &lt;code&gt;COPY&lt;/code&gt; with a column list. With &lt;code&gt;pg_duckdb&lt;/code&gt; you can skip most of that: point &lt;code&gt;read_csv()&lt;/code&gt; / &lt;code&gt;read_json()&lt;/code&gt; at the file in Blob, let DuckDB infer the schema, and &lt;code&gt;CREATE TABLE AS&lt;/code&gt; straight into a normal PostgreSQL table — or just query the file in place and never import it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;Only the Blob secret is needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pg_duckdb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_azure_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;blob connection string&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The files in Blob
&lt;/h2&gt;

&lt;p&gt;A vendor list as CSV and an event stream as newline-delimited JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;az://lake/incoming/vendors.csv
   vendor_id,name,country,active,rating
   1,Acme Corp,US,true,4.5
   2,Globex,CH,false,3.9
   3,Initech,FR,true,4.1
   4,Umbrella,DE,true,2.8
   5,Soylent,JP,false,4.9

az://lake/incoming/events.json
   {"event_id":101,"ts":"2026-01-05T10:00:00","user":"alice","action":"login","value":1}
   {"event_id":102,"ts":"2026-01-05T10:02:13","user":"bob","action":"purchase","value":42}
   {"event_id":103,"ts":"2026-01-05T10:05:41","user":"alice","action":"logout","value":1}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image cover of this blog post shows those files in &lt;a href="https://www.microsoft.com/en-us/download/details.aspx?id=108134" rel="noopener noreferrer"&gt;Microsoft Azure Storage Explorer&lt;/a&gt;. I can read them from &lt;a href="https://azure.microsoft.com/es-es/products/postgresql" rel="noopener noreferrer"&gt;Azure Database for PostgreSQL&lt;/a&gt; with the &lt;code&gt;pg_duckdb&lt;/code&gt; extension enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read a CSV with inferred types
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;read_csv()&lt;/code&gt; parses and infers types from the file — no column list, no cast boilerplate to look at the data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'vendor_id'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'country'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'rating'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/incoming/vendors.csv'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;vendor_id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;   &lt;span class="n"&gt;name&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;rating&lt;/span&gt;
&lt;span class="c1"&gt;-----------+-----------+---------+--------+--------&lt;/span&gt;
 &lt;span class="mi"&gt;1&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Acme&lt;/span&gt; &lt;span class="n"&gt;Corp&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;US&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
 &lt;span class="mi"&gt;2&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Globex&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;CH&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;
 &lt;span class="mi"&gt;3&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Initech&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;FR&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="mi"&gt;4&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Umbrella&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;DE&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;
 &lt;span class="mi"&gt;5&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Soylent&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;JP&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;active&lt;/code&gt; came back as a boolean and &lt;code&gt;rating&lt;/code&gt; as a number — DuckDB inferred them from the file; you didn't declare anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Materialize into a real PostgreSQL table
&lt;/h2&gt;

&lt;p&gt;To bring it in, wrap it in &lt;code&gt;CREATE TABLE AS&lt;/code&gt; and cast each column to the Postgres type you want. (Casting on the way in is good practice — it pins the schema rather than trusting inference, and it gives money/number columns an explicit precision.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;vendors&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
           &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'vendor_id'&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;vendor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'country'&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="nb"&gt;boolean&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'rating'&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rating&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/incoming/vendors.csv'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"vendors" is now an ordinary PostgreSQL table — index it, join it, update it, &lt;code&gt;EXPLAIN&lt;/code&gt; it like any other. Two plans make the point.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;duckdb.force_execution = true&lt;/code&gt;, DuckDB will still &lt;em&gt;scan&lt;/em&gt; the table (because it can), through a &lt;code&gt;TABLE_SCAN&lt;/code&gt; on &lt;code&gt;Table: vendors&lt;/code&gt; — which already proves "vendors" is a real Postgres relation, not a file handle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;force_execution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vendors&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                   &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;-------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;Custom&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DuckDBScan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;021&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;DuckDB&lt;/span&gt; &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="n"&gt;Plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

 &lt;span class="err"&gt;┌─────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌───────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;    &lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;Profiling&lt;/span&gt; &lt;span class="n"&gt;Information&lt;/span&gt;    &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└───────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌────────────────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌──────────────────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;             &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0012&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;              &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└──────────────────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└────────────────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌───────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;QUERY&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="n"&gt;EXPLAIN_ANALYZE&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="n"&gt;UNGROUPED_AGGREGATE&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Aggregates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;count_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;TABLE_SCAN&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="k"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;vendors&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="n"&gt;Filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└───────────────────────────┘&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;196&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
 &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;034&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;TABLE_SCAN&lt;/code&gt; with &lt;code&gt;Table: vendors&lt;/code&gt; and &lt;code&gt;3 rows&lt;/code&gt; (the active vendors) proves "vendors" is a real Postgres relation DuckDB reads, not a file handle. Turn forcing back off (the default) and the data is served entirely by the PostgreSQL executor — no DuckDB in the picture at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;force_execution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;vendors&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                           &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;-----------------------------------------------------------------&lt;/span&gt;
 &lt;span class="k"&gt;Aggregate&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;024&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;025&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
   &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;Seq&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;vendors&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;018&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;019&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;
         &lt;span class="k"&gt;Rows&lt;/span&gt; &lt;span class="n"&gt;Removed&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
         &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;071&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
 &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;062&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;Seq Scan on vendors&lt;/code&gt; is the proof the load worked: the rows are physically in Postgres, queryable by the native engine like any table you created by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same thing for JSON
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;read_json()&lt;/code&gt; handles newline-delimited JSON (and arrays), inferring fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'event_id'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'action'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'value'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/incoming/events.json'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;user&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="n"&gt;action&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;span class="c1"&gt;----------+-------+----------+-------&lt;/span&gt;
 &lt;span class="mi"&gt;101&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;alice&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;login&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="mi"&gt;102&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;bob&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;purchase&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
 &lt;span class="mi"&gt;103&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;alice&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;logout&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And into a table, parsing the timestamp as you go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events_log&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
           &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'event_id'&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'ts'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'user'&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;usr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'action'&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;    &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'value'&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;      &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/incoming/events.json'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;events_log&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_id&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;event_id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="n"&gt;ts&lt;/span&gt;          &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="n"&gt;usr&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="n"&gt;action&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;span class="c1"&gt;----------+---------------------+-------+----------+-------&lt;/span&gt;
 &lt;span class="mi"&gt;101&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;05&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;alice&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;login&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="mi"&gt;102&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;05&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;bob&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;purchase&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
 &lt;span class="mi"&gt;103&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;05&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;05&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;41&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;alice&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;logout&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a recurring load, use &lt;code&gt;INSERT INTO events_log SELECT … FROM read_json(…)&lt;/code&gt; instead of &lt;code&gt;CREATE TABLE AS&lt;/code&gt;, and point the path at the new file (or a glob over a folder of drops — see the &lt;a href="https://dev.to/franckpachot/query-a-partitioned-data-lake-in-azure-blob-with-pgduckdb-31pc"&gt;partitioned-lakes post&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Or don't import at all
&lt;/h2&gt;

&lt;p&gt;Sometimes you don't need the data in Postgres — you just need an answer. You can aggregate the Blob file directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'country'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'rating'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_rating&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/incoming/vendors.csv'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
           &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'country'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
           &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;avg_rating&lt;/span&gt;
&lt;span class="c1"&gt;---------+---+------------&lt;/span&gt;
 &lt;span class="n"&gt;CH&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;
 &lt;span class="n"&gt;DE&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;
 &lt;span class="n"&gt;FR&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="n"&gt;JP&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;
 &lt;span class="n"&gt;US&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the nice property of this approach: the same &lt;code&gt;read_csv()&lt;/code&gt; / &lt;code&gt;read_json()&lt;/code&gt; is either a one-off query or the source of an import, depending on whether you wrap it in &lt;code&gt;CREATE TABLE AS&lt;/code&gt;. Query in place for exploration and ad-hoc checks. Materialize when you need indexes, constraints, or OLTP access.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reverse direction: writing a result back out
&lt;/h2&gt;

&lt;p&gt;Loading isn't one-way. You can also write a Postgres query result back to Blob as Parquet — handy for publishing an aggregate or an extract. On Azure Database for PostgreSQL the plain &lt;code&gt;COPY … TO 'az://…'&lt;/code&gt; syntax is blocked for a normal login (it needs superuser), so you drive the write through the DuckDB engine with &lt;code&gt;duckdb.raw_query&lt;/code&gt;, which authorizes it with the secret and the &lt;code&gt;az://&lt;/code&gt; allow-list — the object-storage controls PostgreSQL's server-file gate has no say over:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$$&lt;/span&gt;
             &lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
                   &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pgduckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vendors&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
             &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'az://lake/exports/vendors_by_country.parquet'&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;parquet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;raw_query&lt;/span&gt;
&lt;span class="c1"&gt;-----------&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;pgduckdb.public.&lt;/code&gt; prefix on the table: inside &lt;code&gt;raw_query&lt;/code&gt; you're in DuckDB's catalog, not PostgreSQL's search path, so a bare &lt;code&gt;vendors&lt;/code&gt; won't resolve. The &lt;a href="https://dev.to/franckpachot/archive-cold-postgres-data-to-azure-blob-and-keep-querying-it-with-pgduckdb-8ej"&gt;archive post&lt;/a&gt; covers this write path in full.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few practical notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cast on import. Inference is convenient for a look, but pin the types in your &lt;code&gt;CREATE TABLE AS&lt;/code&gt; so the table schema is deterministic — and give decimals an explicit precision (&lt;code&gt;numeric(p,s)&lt;/code&gt;), which also keeps later DuckDB queries on the table from falling back (see the &lt;a href="https://dev.to/franckpachot/pgduckdb-quietly-ignored-forceexecution-on-my-numeric-column-h2m"&gt;numeric gotcha side-note&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;CSV options mirror DuckDB's, passed with PostgreSQL named-arg syntax: &lt;code&gt;read_csv('az://…/f.csv', delim := ';', header := true)&lt;/code&gt; and so on.&lt;/li&gt;
&lt;li&gt;Globs work here too. &lt;code&gt;read_csv('az://lake/incoming/*.csv')&lt;/code&gt; loads a whole drop folder in one statement.&lt;/li&gt;
&lt;li&gt;Big loads stream. DuckDB reads the file in chunks, so you're not limited to what fits in memory for the parse.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;read_csv&lt;/code&gt; / &lt;code&gt;read_json&lt;/code&gt; over &lt;code&gt;az://&lt;/code&gt; read files straight from Blob with type inference — no pre-defined table, no external load tool.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CREATE TABLE AS SELECT … FROM read_csv(…)&lt;/code&gt; materializes into a normal Postgres table (cast the columns to pin the schema); the native &lt;code&gt;Seq Scan&lt;/code&gt; plan proves the rows really landed in Postgres. &lt;code&gt;INSERT … SELECT&lt;/code&gt; appends for recurring loads.&lt;/li&gt;
&lt;li&gt;Or skip the import and aggregate the file in place — same function, your choice.&lt;/li&gt;
&lt;li&gt;Globs and DuckDB's CSV options (&lt;code&gt;delim :=&lt;/code&gt;, &lt;code&gt;header :=&lt;/code&gt;, …) are available via PostgreSQL named-argument syntax.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The archive post wrote cold data out to Blob by hand, step by step. In the last post of the series I turn that export-verify-delete cycle into a single durable, scheduled workflow with pg_durable — so a crash mid-way resumes instead of re-exporting.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>duckdb</category>
      <category>azure</category>
      <category>etl</category>
    </item>
    <item>
      <title>pg_duckdb quietly ignored force_execution on my numeric column</title>
      <dc:creator>Franck Pachot</dc:creator>
      <pubDate>Fri, 23 Oct 2026 12:35:08 +0000</pubDate>
      <link>https://dev.to/franckpachot/pgduckdb-quietly-ignored-forceexecution-on-my-numeric-column-h2m</link>
      <guid>https://dev.to/franckpachot/pgduckdb-quietly-ignored-forceexecution-on-my-numeric-column-h2m</guid>
      <description>&lt;p&gt;In this series, I encountered a &lt;code&gt;pg_duckdb&lt;/code&gt; gotcha with unconstrained numeric column. With &lt;code&gt;duckdb.force_execution = true&lt;/code&gt;, I expected DuckDB to run my aggregate. But it ran on the PostgreSQL engine instead, with a warning. Here's how to see it and fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce
&lt;/h2&gt;

&lt;p&gt;I created a table with four columns of the same number, differing only in declared type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;nf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;nf&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;
 &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_bare&lt;/span&gt;  &lt;span class="nb"&gt;numeric&lt;/span&gt;
 &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_10_2&lt;/span&gt;  &lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;       &lt;span class="nb"&gt;double&lt;/span&gt; &lt;span class="nb"&gt;precision&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;nf&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; 
 &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;nf&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here are their Postgres type modifiers (&lt;code&gt;-1&lt;/code&gt; means "no declared precision"):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;attname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;atttypmod&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_attribute&lt;/span&gt;
           &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;attrelid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'nf'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;attnum&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;attisdropped&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;attnum&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;attname&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;atttypmod&lt;/span&gt;
&lt;span class="c1"&gt;---------+-----------&lt;/span&gt;
 &lt;span class="n"&gt;id&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;        &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="n"&gt;n_bare&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt;        &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="n"&gt;n_10_2&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt;    &lt;span class="mi"&gt;655366&lt;/span&gt;
 &lt;span class="n"&gt;d&lt;/span&gt;       &lt;span class="o"&gt;|&lt;/span&gt;        &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;n_bare&lt;/code&gt; and &lt;code&gt;d&lt;/code&gt; show &lt;code&gt;-1&lt;/code&gt; (no declared precision)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;n_10_2&lt;/code&gt; shows &lt;code&gt;655366&lt;/code&gt;, the encoded form of &lt;code&gt;numeric(10,2)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;I forced DuckDB on, then sum the bare numeric:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;force_execution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_bare&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;nf&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;WARNING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PGDuckDB&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;CreatePlan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Prepared&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="n"&gt;returned&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Binder&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;No&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;matches&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;given&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;argument&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt; &lt;span class="s1"&gt;'sum(UnsupportedPostgresType(DuckDB requires the precision of a NUMERIC to be set))'&lt;/span&gt;
                      &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;-----------------------------------------------------&lt;/span&gt;
 &lt;span class="k"&gt;Aggregate&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2074&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;2074&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;Seq&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;nf&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;1824&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a plain PostgreSQL plan. I've set &lt;code&gt;force_execution = true&lt;/code&gt;, yet the top node is a plain &lt;code&gt;Aggregate&lt;/code&gt;, not a &lt;code&gt;Custom Scan (DuckDBScan)&lt;/code&gt;. DuckDB declined and PostgreSQL ran it. A &lt;code&gt;WARNING&lt;/code&gt; at plan time is emitted when &lt;code&gt;pg_duckdb&lt;/code&gt; tries to build the DuckDB plan and bails on the unconstrained numeric. But it's a &lt;code&gt;WARNING&lt;/code&gt;, not an error, easy to miss from an application, and the query still returns the right answer on the native executor. If you were only watching the response time you'd think &lt;code&gt;pg_duckdb&lt;/code&gt; didn't help, when it never ran the query in DuckDB at all.&lt;/p&gt;

&lt;p&gt;I've run the same sum over the &lt;code&gt;numeric(10,2)&lt;/code&gt; column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_10_2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;nf&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                   &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;-------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;Custom&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DuckDBScan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;DuckDB&lt;/span&gt; &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="n"&gt;Plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

 &lt;span class="err"&gt;┌───────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="n"&gt;UNGROUPED_AGGREGATE&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="n"&gt;Aggregates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;n_10_2&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;  &lt;span class="n"&gt;PGDUCKDB_POSTGRES_SCAN&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="k"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;nf&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;n_10_2&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└───────────────────────────┘&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DuckDB runs it. The only difference between the two queries is that one column has a declared precision and the other doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;DuckDB's &lt;code&gt;DECIMAL&lt;/code&gt; is fixed precision, up to 38 digits. A Postgres &lt;code&gt;numeric&lt;/code&gt; with a declared &lt;code&gt;(p,s)&lt;/code&gt; maps straight onto it. A bare &lt;code&gt;numeric&lt;/code&gt; has no bound — there's nothing for DuckDB to map it to — so pg_duckdb leaves the whole query to PostgreSQL. (A &lt;code&gt;numeric(38,10)&lt;/code&gt; works fine, only the unconstrained one falls back.) The fallback itself is a feature: forcing DuckDB can never break a query, it just might not accelerate it. The problem is only that it's silent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;One session setting makes the bare numeric run in DuckDB, read as &lt;code&gt;double&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;convert_unsupported_numeric_to_double&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_bare&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;nf&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                   &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;-------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;Custom&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DuckDBScan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;DuckDB&lt;/span&gt; &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="n"&gt;Plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

 &lt;span class="err"&gt;┌───────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="n"&gt;UNGROUPED_AGGREGATE&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="n"&gt;Aggregates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;n_bare&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;  &lt;span class="n"&gt;PGDUCKDB_POSTGRES_SCAN&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="k"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;nf&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;n_bare&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└───────────────────────────┘&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No more warning, and the top node is now &lt;code&gt;Custom Scan (DuckDBScan)&lt;/code&gt; — DuckDB runs it, reading the bare numeric as &lt;code&gt;double&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The caveat is in the name: &lt;code&gt;double&lt;/code&gt; is floating point, so you can lose the exact decimal precision &lt;code&gt;numeric&lt;/code&gt; guarantees — fine for analytical sums and averages, think twice for exact financial rounding. The cleaner fix is to declare the precision in the schema (&lt;code&gt;numeric(12,2)&lt;/code&gt;), so DuckDB runs it exactly, no flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;force_execution = true&lt;/code&gt;, always &lt;code&gt;EXPLAIN&lt;/code&gt; and check the top node is &lt;code&gt;Custom Scan (DuckDBScan)&lt;/code&gt;. If it's a plain PostgreSQL node, DuckDB declined — and a bare &lt;code&gt;numeric&lt;/code&gt; (typmod &lt;code&gt;-1&lt;/code&gt;) is the usual reason. Declare the precision, or set &lt;code&gt;duckdb.convert_unsupported_numeric_to_double&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; if your can accept that decimal fractions that cannot be stored with exact base-10 precision are rounded.&lt;/p&gt;

&lt;p&gt;Even with &lt;code&gt;duckdb.force_execution = true&lt;/code&gt;, you should always run EXPLAIN to verify that the query utilizes the &lt;code&gt;Custom Scan (DuckDBScan)&lt;/code&gt; node. If the planner falls back to native PostgreSQL nodes, it indicates that DuckDB declined the query—most commonly due to an unconstrained PostgreSQL NUMERIC column (with a type modifier of &lt;code&gt;-1&lt;/code&gt;). To resolve this, explicitly declare the column's precision and scale, or set &lt;code&gt;duckdb.convert_unsupported_numeric_to_double = true&lt;/code&gt; if your workload can tolerate the inherent precision loss and rounding behavior of binary floating-point (DOUBLE) conversions.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>duckdb</category>
      <category>azure</category>
      <category>numeric</category>
    </item>
    <item>
      <title>Query a partitioned data lake in Azure Blob with pg_duckdb</title>
      <dc:creator>Franck Pachot</dc:creator>
      <pubDate>Sat, 17 Oct 2026 21:57:05 +0000</pubDate>
      <link>https://dev.to/franckpachot/query-a-partitioned-data-lake-in-azure-blob-with-pgduckdb-31pc</link>
      <guid>https://dev.to/franckpachot/query-a-partitioned-data-lake-in-azure-blob-with-pgduckdb-31pc</guid>
      <description>&lt;p&gt;A data lake is rarely one file. Once you archive or publish data by period, you get a tree of files — and the trick is querying the whole thing as one table while only reading the parts a query needs. &lt;code&gt;pg_duckdb&lt;/code&gt; does both: globs read many files as one, and Hive-style partitioning turns the folder names into columns and lets DuckDB skip files it doesn't need. With &lt;code&gt;read_parquet()&lt;/code&gt;, the query itself chooses the file set and reader options, rather than requiring a foreign table for each file. Here are some examples from Azure Database for PostgreSQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;Only the Blob secret is needed for this post — everything else reads from the lake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pg_duckdb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_azure_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;blob connection string&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A partitioned layout
&lt;/h2&gt;

&lt;p&gt;The convention: a folder per partition, named &lt;code&gt;column=value&lt;/code&gt;, each holding one or more Parquet files. Here, four yearly files of sales data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;az://lake/sales_part/
   year=2022/data.parquet
   year=2023/data.parquet
   year=2024/data.parquet
   year=2025/data.parquet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You produce these one file per year. The only new thing here is the folder naming (&lt;code&gt;year=YYYY/&lt;/code&gt;). The write from PostgreSQL is covered in the &lt;a href="https://dev.to/franckpachot/archive-cold-postgres-data-to-azure-blob-and-keep-querying-it-with-pgduckdb-8ej"&gt;archive post&lt;/a&gt;. With the &lt;code&gt;sales&lt;/code&gt; table from that post, one year's export looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$$&lt;/span&gt;
  &lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pgduckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-01'&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2023-01-01'&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'az://lake/sales_part/year=2022/data.parquet'&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;parquet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;SELECT&lt;/code&gt; before &lt;code&gt;duckdb.raw_query&lt;/code&gt; and the &lt;code&gt;pgduckdb.public.&lt;/code&gt; catalog prefix: inside &lt;code&gt;raw_query&lt;/code&gt; you're in DuckDB's catalog, not PostgreSQL's. Keep this wrapper on the tested Flexible Server: plain PostgreSQL &lt;code&gt;COPY ... TO 'az://...'&lt;/code&gt; is rejected by the service's file-COPY guard, even though self-managed &lt;code&gt;pg_duckdb&lt;/code&gt; can intercept it. The export also requires permission to execute &lt;code&gt;duckdb.raw_query()&lt;/code&gt;. A client-side export and upload is the alternative described in the archive post.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;column=value&lt;/code&gt; convention lets the reader interpret the folder names as partition columns, and a wildcard selects the files across those folders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read them all with one glob
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;*&lt;/code&gt; in the path reads every matching file as a single table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="s1"&gt;'az://lake/sales_part/year=*/data.parquet'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;count&lt;/span&gt;
&lt;span class="c1"&gt;---------&lt;/span&gt;
 &lt;span class="mi"&gt;8001032&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A specific path reads one partition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="s1"&gt;'az://lake/sales_part/year=2024/data.parquet'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;count&lt;/span&gt;
&lt;span class="c1"&gt;---------&lt;/span&gt;
 &lt;span class="mi"&gt;2003093&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And an explicit list reads an exact set — note the &lt;code&gt;ARRAY[...]&lt;/code&gt; syntax (a bare &lt;code&gt;[...]&lt;/code&gt; is DuckDB dialect that the PostgreSQL parser rejects):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
 &lt;span class="s1"&gt;'az://lake/sales_part/year=2024/data.parquet'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="s1"&gt;'az://lake/sales_part/year=2025/data.parquet'&lt;/span&gt;
           &lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;count&lt;/span&gt;
&lt;span class="c1"&gt;---------&lt;/span&gt;
 &lt;span class="mi"&gt;4003149&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want to know which files a glob actually touched? Add the &lt;code&gt;filename&lt;/code&gt; column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'filename'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="s1"&gt;'az://lake/sales_part/year=*/data.parquet'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
           &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                  &lt;span class="n"&gt;filename&lt;/span&gt;
&lt;span class="c1"&gt;---------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;lake&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;sales_part&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;year&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2022&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parquet&lt;/span&gt;
 &lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;lake&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;sales_part&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;year&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2023&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parquet&lt;/span&gt;
 &lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;lake&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;sales_part&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;year&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2024&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parquet&lt;/span&gt;
 &lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;lake&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;sales_part&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;year&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parquet&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(pg_duckdb passes DuckDB's optional arguments with PostgreSQL's named-argument syntax, &lt;code&gt;name := value&lt;/code&gt; — so &lt;code&gt;filename := true&lt;/code&gt;, &lt;code&gt;hive_partitioning := true&lt;/code&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the folder name into a column
&lt;/h2&gt;

&lt;p&gt;The partition value lives only in the path, not inside the files. Turn on &lt;code&gt;hive_partitioning&lt;/code&gt; and &lt;code&gt;year&lt;/code&gt; becomes a real column you can group and filter on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'year'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="s1"&gt;'az://lake/sales_part/year=*/data.parquet'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;hive_partitioning&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
           &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'year'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
           &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;    &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="c1"&gt;------+---------&lt;/span&gt;
 &lt;span class="mi"&gt;2022&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1998570&lt;/span&gt;
 &lt;span class="mi"&gt;2023&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1999313&lt;/span&gt;
 &lt;span class="mi"&gt;2024&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2003093&lt;/span&gt;
 &lt;span class="mi"&gt;2025&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2000056&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the year is encoded in the layout, you can compute per-year totals even without the files carrying a date column at all.&lt;/p&gt;

&lt;p&gt;Partition values are not necessarily text. On this server (PostgreSQL 18.6, pg_duckdb 1.1.0), &lt;code&gt;SELECT pg_typeof(r['year'])&lt;/code&gt; over this reader returns &lt;code&gt;bigint&lt;/code&gt;: DuckDB infers the type from the folder values. Both &lt;code&gt;r['year'] = 2024&lt;/code&gt; and the quoted literal below work and prune to one file.&lt;/p&gt;

&lt;h2&gt;
  
  
  When file schemas evolve
&lt;/h2&gt;

&lt;p&gt;If a later year's file adds a column, use &lt;code&gt;union_by_name := true&lt;/code&gt; to align columns by name rather than position:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'az://lake/sales_part/year=*/data.parquet'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;hive_partitioning&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;union_by_name&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Columns absent from a file become &lt;code&gt;NULL&lt;/code&gt; for that file's rows. This handles added or missing columns, not arbitrary incompatible type changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff: partition pruning, visible in the plan
&lt;/h2&gt;

&lt;p&gt;This is why you partition. Filter on the partition column and DuckDB reads only the matching files. &lt;code&gt;EXPLAIN&lt;/code&gt; shows it — and here is the full verbatim plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/sales_part/year=*/data.parquet'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                             &lt;span class="n"&gt;hive_partitioning&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
           &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'year'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2024'&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                   &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;-------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;Custom&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DuckDBScan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;002&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;DuckDB&lt;/span&gt; &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="n"&gt;Plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

 &lt;span class="err"&gt;┌─────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌───────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;    &lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;Profiling&lt;/span&gt; &lt;span class="n"&gt;Information&lt;/span&gt;    &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└───────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌────────────────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌──────────────────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;              &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;78&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;               &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└──────────────────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└────────────────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌───────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;QUERY&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="n"&gt;EXPLAIN_ANALYZE&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="n"&gt;UNGROUPED_AGGREGATE&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Aggregates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;count_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;TABLE_SCAN&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="k"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;READ_PARQUET&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="n"&gt;File&lt;/span&gt; &lt;span class="n"&gt;Filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="n"&gt;Scanning&lt;/span&gt; &lt;span class="n"&gt;Files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="n"&gt;Files&lt;/span&gt; &lt;span class="k"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;003&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;093&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└───────────────────────────┘&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;329&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;131&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
 &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;330&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;574&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lines that matter are &lt;code&gt;Scanning Files: 1/4&lt;/code&gt; and &lt;code&gt;Total Files Read: 1&lt;/code&gt;. The glob matched four files, but the &lt;code&gt;year = 2024&lt;/code&gt; filter let DuckDB skip scanning data from three of them — it decided that from the folder names alone (&lt;code&gt;File Filters: (year = 2024)&lt;/code&gt;). This does not mean zero listing or metadata requests: resolving the glob and binding the reader can still require them. The &lt;code&gt;2,003,093 rows&lt;/code&gt; at the scan is the one file's actual row count; a &lt;code&gt;count(*)&lt;/code&gt; can use Parquet metadata rather than decode every row. With hundreds of monthly partitions, a filter selecting one month can restrict the data scan to that month's files, whether there is one file or several. That's why this layout pays off, and pg_duckdb gives it to you over Azure Blob with the filter sitting in ordinary SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does &lt;code&gt;abfss://&lt;/code&gt; work on Azure Database for PostgreSQL?
&lt;/h2&gt;

&lt;p&gt;DuckDB's Azure extension supports both Blob (&lt;code&gt;az://&lt;/code&gt;) and ADLS Gen2 (&lt;code&gt;abfss://&lt;/code&gt;) paths, but the managed server's allow-list comes first. I retested on this Flexible Server with PostgreSQL 18.6 and pg_duckdb 1.1.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable_external_access&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable_external_access&lt;/span&gt;
&lt;span class="c1"&gt;-------------------------------&lt;/span&gt;
 &lt;span class="k"&gt;off&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allowed_directories&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allowed_directories&lt;/span&gt;
&lt;span class="c1"&gt;----------------------------&lt;/span&gt;
 &lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'abfss://lake/sales_part/year=2024/data.parquet'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PGDuckDB&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;CreatePlan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Prepared&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="n"&gt;returned&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Permission&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Cannot&lt;/span&gt; &lt;span class="k"&gt;access&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="nv"&gt;"abfss://lake/sales_part/year=2024/data.parquet"&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt; &lt;span class="n"&gt;operations&lt;/span&gt; &lt;span class="k"&gt;are&lt;/span&gt; &lt;span class="n"&gt;disabled&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;

&lt;span class="n"&gt;LINE&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'abfss://lake/sales_part/year=2024...
                                      ^
postgres=&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both a single-file read and a glob fail, including with the fully qualified &lt;code&gt;abfss://&amp;lt;account&amp;gt;.dfs.core.windows.net/lake/...&lt;/code&gt; form. The same reads through &lt;code&gt;az://&lt;/code&gt; succeed. These settings require superuser privileges to change, which this login does not have.&lt;/p&gt;

&lt;p&gt;So the two schemes are not interchangeable on this server. The rejection happens in DuckDB's filesystem permission check, before we can test the ADLS endpoint or compare listing performance. Use &lt;code&gt;az://&lt;/code&gt; here; there is no measured basis in this test for recommending &lt;code&gt;abfss://&lt;/code&gt; as faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A glob (&lt;code&gt;year=*/data.parquet&lt;/code&gt;) reads a whole partitioned lake as one table. A path reads one partition. &lt;code&gt;ARRAY[...]&lt;/code&gt; reads an explicit set.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hive_partitioning := true&lt;/code&gt; turns &lt;code&gt;column=value&lt;/code&gt; folder names into queryable columns — group and filter on them like any column.&lt;/li&gt;
&lt;li&gt;Filtering the partition column prunes files before reading: the plan shows &lt;code&gt;Scanning Files: 1/4&lt;/code&gt;. This is the main reason partitioning pays off.&lt;/li&gt;
&lt;li&gt;Pass DuckDB's reader options with PostgreSQL named-arg syntax (&lt;code&gt;:=&lt;/code&gt;); use &lt;code&gt;ARRAY[...]&lt;/code&gt; not &lt;code&gt;[...]&lt;/code&gt; for file lists.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;union_by_name := true&lt;/code&gt; when files add or omit columns across partitions.&lt;/li&gt;
&lt;li&gt;On the tested Flexible Server, &lt;code&gt;az://&lt;/code&gt; is allowed but &lt;code&gt;abfss://&lt;/code&gt; is blocked by configuration, for both single files and globs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything so far has read Parquet. Blob also collects the messier arrivals — vendor CSVs, newline-delimited JSON event streams. Next I'll point &lt;code&gt;read_csv&lt;/code&gt; and &lt;code&gt;read_json&lt;/code&gt; at those and load them into Postgres with a single statement.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>duckdb</category>
      <category>azure</category>
      <category>datalake</category>
    </item>
    <item>
      <title>GDPR data minimization with pg_duckdb: join Azure Blob facts to erasable customer data in Postgres</title>
      <dc:creator>Franck Pachot</dc:creator>
      <pubDate>Thu, 15 Oct 2026 07:59:08 +0000</pubDate>
      <link>https://dev.to/franckpachot/gdpr-data-minimization-with-pgduckdb-join-azure-blob-facts-to-erasable-customer-data-in-postgres-4ce8</link>
      <guid>https://dev.to/franckpachot/gdpr-data-minimization-with-pgduckdb-join-azure-blob-facts-to-erasable-customer-data-in-postgres-4ce8</guid>
      <description>&lt;p&gt;After the &lt;a href="https://dev.to/franckpachot/archive-cold-postgres-data-to-azure-blob-and-keep-querying-it-with-pgduckdb-8ej"&gt;archive post&lt;/a&gt;, I got a good question: what happens when an old row must change after its year has been written to Parquet? Rewriting a large immutable file for every correction or deletion would defeat much of the purpose of the archive.&lt;/p&gt;

&lt;p&gt;My first answer is to archive a fact only once the business considers it immutable. Historical sales shouldn’t change silently after reports are published. Personal data, however, follows a different lifecycle: a customer's name, email, phone number, or address may need to be corrected or erased even if the financial fact must remain unchanged.&lt;/p&gt;

&lt;p&gt;That suggests a useful split:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2uyvggnua7ssnp7jew9q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2uyvggnua7ssnp7jew9q.png" alt=" " width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The immutable sales facts go to Parquet. The identifying data remains in PostgreSQL, within the operational system's existing security and governance boundary: database roles and row-level rules, audited changes, constraints, transactional corrections and deletions, retention procedures, and controlled backup and restore processes. &lt;code&gt;pg_duckdb&lt;/code&gt; joins both sources only when a query requires them.&lt;/p&gt;

&lt;p&gt;The point is not that object storage cannot be secured. It is to avoid creating immutable analytical files from personal data, each of which would require its own access controls, audit trail, retention policy, backup handling, and erasure procedure. Under GDPR's data-minimization principle, keeping direct identifiers out of the archive also reduces the amount of personal data copied into the analytical tier.&lt;/p&gt;

&lt;p&gt;This is where &lt;code&gt;pg_duckdb&lt;/code&gt; adds more than just convenient file syntax. The PostgreSQL table and the Parquet file become two scan inputs for a single DuckDB relational plan. DuckDB chooses the join order and build side, pushes filters toward both sources, performs the hash join, and runs the aggregation in a single vectorized pipeline.&lt;/p&gt;

&lt;p&gt;The query uses a &lt;code&gt;LEFT JOIN&lt;/code&gt; with the archived facts on the preserved side, so deleting the matching identity leaves the sale in the result with &lt;code&gt;NULL&lt;/code&gt; values in the personal columns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lab setup
&lt;/h2&gt;

&lt;p&gt;The sales data is the same deterministic 10-million-row dataset used in the previous posts. There is no &lt;code&gt;random()&lt;/code&gt;, so the counts below are reproducible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;force_execution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sale_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2021-01-01'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;1826&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;                        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                               &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cust_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;prod_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'US'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'CH'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'FR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'DE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'UK'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'JP'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'BR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'IN'&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Consumer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'SMB'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Enterprise'&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;))::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;                              &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10000000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pg_duckdb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_azure_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;blob connection string&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build the source table on the PostgreSQL executor (&lt;code&gt;force_execution = false&lt;/code&gt;). If DuckDB is forced on, it warns about &lt;code&gt;+(DATE, BIGINT)&lt;/code&gt; in this &lt;code&gt;generate_series&lt;/code&gt; DDL and falls back to PostgreSQL anyway. You don't need to force DuckDB for this example.&lt;/p&gt;

&lt;p&gt;Now create the table that owns the erasable identity. The values are synthetic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;customer_identity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customer_identity&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;full_name&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;phone&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;address&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;customer_identity&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Customer %s'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lpad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
       &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'customer%s@example.test'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lpad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
       &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'+41 22 555 %s'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lpad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
       &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%s Example Street, Test City'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;customer_identity_email_idx&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;customer_identity&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;customer_identity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For readability, this lab reuses the sequential &lt;code&gt;cust_id&lt;/code&gt; as &lt;code&gt;customer_ref&lt;/code&gt;. In a real design, use a stable, opaque token with no business meaning, and keep the only mapping to an identity in the protected operational database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write a minimized fact file
&lt;/h2&gt;

&lt;p&gt;When exporting the 2021 sales data, select only the immutable fact columns. Do not copy the name, email, phone, address, or other identifying free text into the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$$&lt;/span&gt;
             &lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
               &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;sale_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;cust_id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;prod_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;amount&lt;/span&gt;
               &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pgduckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;
               &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-01'&lt;/span&gt;
             &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'az://lake/archive/sales_2021_minimized.parquet'&lt;/span&gt;
               &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;parquet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;raw_query&lt;/span&gt;
&lt;span class="c1"&gt;-----------&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As in the archive post, &lt;code&gt;raw_query&lt;/code&gt; sends the &lt;code&gt;COPY&lt;/code&gt; command to the embedded DuckDB engine, and &lt;code&gt;pgduckdb.public.sales&lt;/code&gt; is the DuckDB catalog name for the PostgreSQL table.&lt;/p&gt;

&lt;p&gt;Check both sides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customer_identity&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="k"&gt;count&lt;/span&gt;
&lt;span class="c1"&gt;-------&lt;/span&gt;
 &lt;span class="mi"&gt;50000&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n_sales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'customer_ref'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n_customers&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="s1"&gt;'az://lake/archive/sales_2021_minimized.parquet'&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;n_sales&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n_customers&lt;/span&gt;
&lt;span class="c1"&gt;---------+-------------&lt;/span&gt;
 &lt;span class="mi"&gt;1998938&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file has 1,998,938 immutable sales and only an opaque bridge to the 50,000 identity rows kept in PostgreSQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  The join before erasure
&lt;/h2&gt;

&lt;p&gt;Customer 42 has 39 sales in the archived year. The query resolves the bridge to the live identity using a &lt;code&gt;LEFT OUTER JOIN&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
             &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[erased customer]'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[removed]'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n_sales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="s1"&gt;'az://lake/archive/sales_2021_minimized.parquet'&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
           &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;OUTER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;customer_identity&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
             &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'customer_ref'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;
           &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'customer_ref'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
           &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;customer_name&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;                      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n_sales&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
&lt;span class="c1"&gt;----------------+----------------------------+---------+---------&lt;/span&gt;
 &lt;span class="n"&gt;Customer&lt;/span&gt; &lt;span class="mi"&gt;00042&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;customer00042&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;39&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1794&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;SET duckdb.force_execution = true&lt;/code&gt; is required. DuckDB automatically executes any statement that references &lt;code&gt;read_parquet&lt;/code&gt;, including the scan of the PostgreSQL table and the join.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delete the identity, not the history
&lt;/h2&gt;

&lt;p&gt;Now process the erasure in the operational database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customer_identity&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run exactly the same join again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
             &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[erased customer]'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;customer_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'[removed]'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n_sales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="s1"&gt;'az://lake/archive/sales_2021_minimized.parquet'&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
           &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;OUTER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;customer_identity&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
             &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'customer_ref'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;
           &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'customer_ref'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
           &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;customer_name&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;     &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n_sales&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
&lt;span class="c1"&gt;-------------------+-----------+---------+---------&lt;/span&gt;
 &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;erased&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;removed&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;39&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1794&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 39 sales and their 1,794.00 in revenue remain. The name and email are gone. I used COALESCE to show a value for nulls, but NULL is semantically correct: the customer exists, but their personal data is unknown.&lt;/p&gt;

&lt;p&gt;After the deletion, all 1,998,938 archived facts are still counted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
             &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'erased'&lt;/span&gt;
                  &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'identified'&lt;/span&gt;
             &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;identity_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n_sales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="s1"&gt;'az://lake/archive/sales_2021_minimized.parquet'&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
           &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;customer_identity&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
             &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'customer_ref'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;
           &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'erased'&lt;/span&gt;
                         &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'identified'&lt;/span&gt;
                    &lt;span class="k"&gt;END&lt;/span&gt;
           &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;identity_status&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;identity_status&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n_sales&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
&lt;span class="c1"&gt;-----------------+---------+--------------&lt;/span&gt;
 &lt;span class="n"&gt;erased&lt;/span&gt;          &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;39&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1794&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;
 &lt;span class="n"&gt;identified&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1998899&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;508736609&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's always important to review the execution plan because SQL code is an abstraction that doesn't show how it's executed or how performant and scalable it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full plan: which side is hashed?
&lt;/h2&gt;

&lt;p&gt;Here is the full &lt;code&gt;EXPLAIN (ANALYZE, COSTS OFF)&lt;/code&gt; from Azure Database for PostgreSQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;SELECT&lt;/span&gt;
             &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'erased'&lt;/span&gt;
                  &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'identified'&lt;/span&gt;
             &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;identity_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n_sales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="s1"&gt;'az://lake/archive/sales_2021_minimized.parquet'&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
           &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;OUTER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;customer_identity&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
             &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'customer_ref'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;
           &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'erased'&lt;/span&gt;
                         &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'identified'&lt;/span&gt;
                    &lt;span class="k"&gt;END&lt;/span&gt;
           &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;identity_status&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                   &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;-------------------------------------------------&lt;/span&gt;
&lt;span class="n"&gt;Custom&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DuckDBScan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;003&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;DuckDB&lt;/span&gt; &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="n"&gt;Plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="err"&gt;┌─────────────────────────────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│┌───────────────────────────────────┐│&lt;/span&gt;
&lt;span class="err"&gt;││&lt;/span&gt;    &lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;Profiling&lt;/span&gt; &lt;span class="n"&gt;Information&lt;/span&gt;    &lt;span class="err"&gt;││&lt;/span&gt;
&lt;span class="err"&gt;│└───────────────────────────────────┘│&lt;/span&gt;
&lt;span class="err"&gt;└─────────────────────────────────────┘&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'erased'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'identified'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;identity_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n_sales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/archive/sales_2021_minimized.parquet'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pgduckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_identity&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'erased'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'identified'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'erased'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'identified'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;
&lt;span class="err"&gt;┌────────────────────────────────────────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│┌──────────────────────────────────────────────┐│&lt;/span&gt;
&lt;span class="err"&gt;││&lt;/span&gt;              &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;515&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;              &lt;span class="err"&gt;││&lt;/span&gt;
&lt;span class="err"&gt;│└──────────────────────────────────────────────┘│&lt;/span&gt;
&lt;span class="err"&gt;└────────────────────────────────────────────────┘&lt;/span&gt;
&lt;span class="err"&gt;┌───────────────────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;QUERY&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
&lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="n"&gt;EXPLAIN_ANALYZE&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
&lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="n"&gt;__internal_decompress_strin&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
&lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="n"&gt;ORDER_BY&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="k"&gt;CASE&lt;/span&gt;  &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'erased'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;  &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;  &lt;span class="s1"&gt;'identified'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
&lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="n"&gt;__internal_compress_string_&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;uhugeint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
&lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="n"&gt;__internal_decompress_strin&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
&lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="n"&gt;HASH_GROUP_BY&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;Groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Aggregates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;count_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
&lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="n"&gt;__internal_compress_string_&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="n"&gt;uhugeint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;  &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'erased'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;     &lt;span class="s1"&gt;'identified'&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;amount&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;998&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;938&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
&lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;HASH_JOIN&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="k"&gt;Join&lt;/span&gt; &lt;span class="k"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Conditions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="err"&gt;├──────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customer_ref&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;998&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;938&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;08&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;┌─────────────┴─────────────┐┌─────────────┴─────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;TABLE_SCAN&lt;/span&gt;        &lt;span class="err"&gt;││&lt;/span&gt;         &lt;span class="n"&gt;TABLE_SCAN&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;││&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="k"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="err"&gt;││&lt;/span&gt;           &lt;span class="k"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;READ_PARQUET&lt;/span&gt;       &lt;span class="err"&gt;││&lt;/span&gt;     &lt;span class="n"&gt;customer_identity&lt;/span&gt;     &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;││&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="err"&gt;││&lt;/span&gt;        &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;customer_ref&lt;/span&gt;       &lt;span class="err"&gt;││&lt;/span&gt;        &lt;span class="n"&gt;customer_ref&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;amount&lt;/span&gt;          &lt;span class="err"&gt;││&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;││&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="n"&gt;Files&lt;/span&gt; &lt;span class="k"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;    &lt;span class="err"&gt;││&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;││&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;998&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;938&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;      &lt;span class="err"&gt;││&lt;/span&gt;        &lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;999&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;││&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;└───────────────────────────┘└───────────────────────────┘&lt;/span&gt;


&lt;span class="n"&gt;Planning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;223&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;697&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;223&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;914&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;126&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One top-level engine over heterogeneous sources
&lt;/h2&gt;

&lt;p&gt;The outer PostgreSQL plan is a single &lt;code&gt;Custom Scan (DuckDBScan)&lt;/code&gt;. Inside it, the DuckDB plan includes both leaves—the Blob &lt;code&gt;READ_PARQUET&lt;/code&gt; scan and the PostgreSQL &lt;code&gt;customer_identity&lt;/code&gt; scan—and the &lt;code&gt;HASH_JOIN&lt;/code&gt; and &lt;code&gt;HASH_GROUP_BY&lt;/code&gt; above them. DuckDB, not PostgreSQL, owns the cross-source join and aggregation.&lt;/p&gt;

&lt;p&gt;That is different from the read-only foreign-data-wrapper solution. With a conventional FDW, PostgreSQL remains the top-level relational engine: the foreign side returns rows or a pushed-down fragment, and PostgreSQL typically joins the result with local tables. Here, DuckDB sees both sides together and can optimize the pipeline across the storage boundary, including the runtime join filter. Everything between the two scans and the result is one vectorized pipeline: the Parquet scan streams ~2M rows, probes the hash table, and HASH_GROUP_BY reduces them to 2 rows without materializing the join output.&lt;/p&gt;

&lt;p&gt;DuckDB, a single engine, executes the query with all its analytics optimizations, while PostgreSQL continues to read its own storage. The &lt;code&gt;PGDUCKDB_POSTGRES_SCAN&lt;/code&gt; leaf delegates to SQL, where the PostgreSQL optimizer chooses the heap or index access path. PostgreSQL handles only this scan leaf. DuckDB controls the join order, hash table, filtering between inputs, grouping, and final result.&lt;/p&gt;

&lt;p&gt;Read a DuckDB plan bottom-up. A physical &lt;code&gt;HASH_JOIN&lt;/code&gt; builds its in-memory hash table from the right child, then streams the left child through it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The right child is &lt;code&gt;customer_identity&lt;/code&gt;: 49,999 PostgreSQL rows, with only &lt;code&gt;customer_ref&lt;/code&gt; projected. This is the side that is hashed.&lt;/li&gt;
&lt;li&gt;The left child is &lt;code&gt;READ_PARQUET&lt;/code&gt;: 1,998,938 Blob rows, with only &lt;code&gt;customer_ref&lt;/code&gt; and &lt;code&gt;amount&lt;/code&gt; projected. This is the probe side.&lt;/li&gt;
&lt;li&gt;The join emits all 1,998,938 facts, then &lt;code&gt;HASH_GROUP_BY&lt;/code&gt; reduces them to two rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hashing 49,999 integers once is the natural shape. A nested loop would prevent DuckDB from performing a B-tree lookup in Parquet. It would replace one small hash build with repeated file probes. Even when the customer filter below leaves one row, DuckDB keeps a hash join because hashing one key is negligible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What statistics does DuckDB see?
&lt;/h2&gt;

&lt;p&gt;An EXPLAIN without ANALYZE (the default, or explicitly &lt;code&gt;EXPLAIN (ANALYZE OFF, COSTS ON)&lt;/code&gt;) displays the estimated cardinalities (DuckDB prefixes them with &lt;code&gt;~&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;
 &lt;span class="n"&gt;Custom&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DuckDBScan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;DuckDB&lt;/span&gt; &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="n"&gt;Plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

 &lt;span class="err"&gt;┌───────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="n"&gt;__internal_decompress_strin&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="n"&gt;ORDER_BY&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="k"&gt;CASE&lt;/span&gt;  &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'erased'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;  &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;  &lt;span class="s1"&gt;'identified'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="n"&gt;__internal_compress_string_&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;uhugeint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;469&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="n"&gt;__internal_decompress_strin&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;469&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="n"&gt;HASH_GROUP_BY&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;Groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Aggregates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;count_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;469&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="n"&gt;__internal_compress_string_&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="n"&gt;uhugeint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt;  &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'erased'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;     &lt;span class="s1"&gt;'identified'&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;amount&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;998&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;938&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;HASH_JOIN&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="k"&gt;Join&lt;/span&gt; &lt;span class="k"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;LEFT&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Conditions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="err"&gt;├──────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customer_ref&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;998&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;938&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="n"&gt;READ_PARQUET&lt;/span&gt;        &lt;span class="err"&gt;││&lt;/span&gt;  &lt;span class="n"&gt;PGDUCKDB_POSTGRES_SCAN&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;││&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="k"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="err"&gt;││&lt;/span&gt;           &lt;span class="k"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;READ_PARQUET&lt;/span&gt;       &lt;span class="err"&gt;││&lt;/span&gt;     &lt;span class="n"&gt;customer_identity&lt;/span&gt;     &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;││&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="err"&gt;││&lt;/span&gt;        &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;customer_ref&lt;/span&gt;       &lt;span class="err"&gt;││&lt;/span&gt;        &lt;span class="n"&gt;customer_ref&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;amount&lt;/span&gt;          &lt;span class="err"&gt;││&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;││&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;998&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;938&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;      &lt;span class="err"&gt;││&lt;/span&gt;        &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└───────────────────────────┘└───────────────────────────┘&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those estimates come from different places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parquet carries its row counts and per-row-group min/max statistics in the file metadata.&lt;/li&gt;
&lt;li&gt;For a PostgreSQL relation, pg_duckdb 1.1.0 calls PostgreSQL's &lt;a href="https://github.com/duckdb/pg_duckdb/blob/59efdd9d9e0e1d97993c6db88f1f7adc37a91ad4/src/pg/relations.cpp#L114-L122" rel="noopener noreferrer"&gt;&lt;code&gt;estimate_rel_size&lt;/code&gt;&lt;/a&gt; and passes that cardinality to DuckDB.&lt;/li&gt;
&lt;li&gt;It does not expose PostgreSQL's per-column histograms, most-common values, or distinct-value estimates to DuckDB (&lt;a href="https://github.com/duckdb/pg_duckdb/blob/59efdd9d9e0e1d97993c6db88f1f7adc37a91ad4/src/catalog/pgduckdb_table.cpp#L50-L52" rel="noopener noreferrer"&gt;&lt;code&gt;PostgresTable::GetStatistics&lt;/code&gt;&lt;/a&gt; is not implemented in this release).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This explains the mixed quality of the estimates. DuckDB knows the inputs contain about 2 million and 50,000 rows, enough to place the small relation on the hash-build side. It does not predict that the final &lt;code&gt;CASE&lt;/code&gt; expression has exactly two groups: the plain plan estimates &lt;code&gt;~999,469 groups&lt;/code&gt;, whereas the analyzed plan produces &lt;code&gt;2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ANALYZE customer_identity&lt;/code&gt; still matters. PostgreSQL statistics maintain the relation-size estimate supplied to DuckDB, and PostgreSQL's own planner uses its full local statistics when &lt;code&gt;pg_duckdb&lt;/code&gt; delegates a filtered scan back to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if the filter starts from the customer?
&lt;/h2&gt;

&lt;p&gt;The setup already created &lt;code&gt;customer_identity_email_idx&lt;/code&gt;. Now ask for the archived sales of one email address. This deliberately uses an INNER JOIN: the question asks for one known identity, so unmatched or erased identities are irrelevant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;SELECT&lt;/span&gt;
             &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n_sales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customer_identity&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
           &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="s1"&gt;'az://lake/archive/sales_2021_minimized.parquet'&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
             &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'customer_ref'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt;
           &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customer00043@example.test'&lt;/span&gt;
           &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full_name&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

                                                                                                                                                                             &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;                                                                                                                                                                 
&lt;span class="c1"&gt;--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;Custom&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DuckDBScan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;002&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;002&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;DuckDB&lt;/span&gt; &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="n"&gt;Plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

 &lt;span class="err"&gt;┌─────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌───────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;    &lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;Profiling&lt;/span&gt; &lt;span class="n"&gt;Information&lt;/span&gt;    &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└───────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n_sales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pgduckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_identity&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/archive/sales_2021_minimized.parquet'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;integer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customer00043@example.test'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full_name&lt;/span&gt;
 &lt;span class="err"&gt;┌────────────────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌──────────────────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;              &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;437&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;              &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└──────────────────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└────────────────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌───────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;QUERY&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="n"&gt;EXPLAIN_ANALYZE&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="n"&gt;HASH_GROUP_BY&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;Groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Aggregates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;count_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;full_name&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;amount&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="mi"&gt;39&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;HASH_JOIN&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="k"&gt;Join&lt;/span&gt; &lt;span class="k"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;INNER&lt;/span&gt;     &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Conditions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="err"&gt;├──────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customer_ref&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="mi"&gt;39&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;TABLE_SCAN&lt;/span&gt;        &lt;span class="err"&gt;││&lt;/span&gt;         &lt;span class="n"&gt;TABLE_SCAN&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;││&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="k"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="err"&gt;││&lt;/span&gt;           &lt;span class="k"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;READ_PARQUET&lt;/span&gt;       &lt;span class="err"&gt;││&lt;/span&gt;     &lt;span class="n"&gt;customer_identity&lt;/span&gt;     &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;││&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="err"&gt;││&lt;/span&gt;        &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;customer_ref&lt;/span&gt;       &lt;span class="err"&gt;││&lt;/span&gt;        &lt;span class="n"&gt;customer_ref&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;amount&lt;/span&gt;          &lt;span class="err"&gt;││&lt;/span&gt;         &lt;span class="n"&gt;full_name&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;││&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="n"&gt;Files&lt;/span&gt; &lt;span class="k"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;    &lt;span class="err"&gt;││&lt;/span&gt;          &lt;span class="n"&gt;Filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;││&lt;/span&gt;    &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'customer00043   │
 │                           ││       @example.test'&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;││&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="mi"&gt;39&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;││&lt;/span&gt;           &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;││&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└───────────────────────────┘└───────────────────────────┘&lt;/span&gt;


 &lt;span class="n"&gt;Planning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;566&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;542&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
 &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;213&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;829&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;78&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bottom of &lt;code&gt;EXPLAIN (ANALYZE, COSTS OFF)&lt;/code&gt; is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────┴─────────────┐
│         HASH_JOIN         │
│    ────────────────────   │
│      Join Type: INNER     │
│                           │
│        Conditions:        ├──────────────┐
│customer_ref = customer_ref│              │
│                           │              │
│          39 rows          │              │
└─────────────┬─────────────┘              │
┌─────────────┴─────────────┐┌─────────────┴─────────────┐
│         TABLE_SCAN        ││         TABLE_SCAN        │
│    ────────────────────   ││    ────────────────────   │
│         Function:         ││           Table:          │
│        READ_PARQUET       ││     customer_identity     │
│                           ││                           │
│        Projections:       ││        Projections:       │
│        customer_ref       ││        customer_ref       │
│           amount          ││         full_name         │
│                           ││                           │
│    Total Files Read: 1    ││          Filters:         │
│                           ││    email='customer00043   │
│                           ││       @example.test'      │
│                           ││                           │
│          39 rows          ││           1 row           │
└───────────────────────────┘└───────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It starts with the selective customer side. The right child produces one row and becomes the hash-build side. DuckDB then applies the resulting join-key range while scanning Parquet: the &lt;code&gt;READ_PARQUET&lt;/code&gt; leaf emits 39 rows, not 1,998,938. The runtime join filter is not printed as a static &lt;code&gt;Filters:&lt;/code&gt; line in this plan, but the scan's actual row count makes it visible. When the predicate is written directly on the file side, the plan does print &lt;code&gt;Filters: customer_ref=43&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Many databases push join filters only through a nested loop. In PostgreSQL, for example, each outer row re-parameterizes an inner index scan. YugabyteDB batches these scans to amortize the probes. Oracle and SQL Server instead attach a Bloom or bitmap filter to the hash join, allowing the probe scan to discard non-matching keys. DuckDB takes this latter approach, optimized for analytics: while building a hash table from the one-row customer_identity side, it derives a dynamic filter on the join key and injects it as a table filter into the probe-side READ_PARQUET scan.&lt;/p&gt;

&lt;p&gt;The PostgreSQL table is not a blind full scan either. &lt;code&gt;duckdb.log_pg_explain&lt;/code&gt; shows the inner SQL that pg_duckdb delegated and the access path PostgreSQL chose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log_pg_explain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;
             &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n_sales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customer_identity&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;
           &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="s1"&gt;'az://lake/archive/sales_2021_minimized.parquet'&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
             &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'customer_ref'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_ref&lt;/span&gt;
           &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customer00043@example.test'&lt;/span&gt;
           &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full_name&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;NOTICE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PGDuckDB&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;PostgresTableReader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;QUERY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_identity&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'customer00043@example.test'&lt;/span&gt;

&lt;span class="n"&gt;RUNNING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="n"&gt;PROCESS&lt;/span&gt; &lt;span class="n"&gt;THREAD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

&lt;span class="n"&gt;EXECUTING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="n"&gt;Parallel&lt;/span&gt; &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="n"&gt;customer_identity_email_idx&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;customer_identity&lt;/span&gt;
  &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Cond&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customer00043@example.test'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

 &lt;span class="n"&gt;full_name&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n_sales&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
&lt;span class="c1"&gt;----------------+---------+---------&lt;/span&gt;
 &lt;span class="n"&gt;Customer&lt;/span&gt; &lt;span class="mi"&gt;00043&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;39&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1833&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DuckDB does not manage PostgreSQL indexes directly. It pushes columns and predicates into an inner PostgreSQL query, allowing the PostgreSQL optimizer to choose a sequential, bitmap, index, or index-only scan. Here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;full_name&lt;/span&gt;
       &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_identity&lt;/span&gt;
       &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'customer00043@example.test'&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                            &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;-------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="n"&gt;customer_identity_email_idx&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;customer_identity&lt;/span&gt;
   &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Cond&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customer00043@example.test'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the unfiltered full join above, which requires only &lt;code&gt;customer_ref&lt;/code&gt;, the delegated plan was a &lt;code&gt;Parallel Index Only Scan using customer_identity_pkey on customer_identity&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_ref&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_identity&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                                    &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;----------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;Gather&lt;/span&gt;
   &lt;span class="n"&gt;Workers&lt;/span&gt; &lt;span class="n"&gt;Planned&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
   &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;Parallel&lt;/span&gt; &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="k"&gt;Only&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="n"&gt;customer_identity_pkey&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;customer_identity&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flow works in the other direction too. With &lt;code&gt;WHERE (s['customer_ref'])::int = 43&lt;/code&gt; on the Parquet side, the delegated SQL became &lt;code&gt;SELECT customer_ref, full_name FROM public.customer_identity WHERE customer_ref=43&lt;/code&gt;, and PostgreSQL chose &lt;code&gt;Parallel Index Scan using customer_identity_pkey&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Using DuckDB over PostgreSQL via &lt;code&gt;pg_duckdb&lt;/code&gt;, rather than PostgreSQL over DuckDB via a foreign data wrapper, is a major advantage here. DuckDB was designed for analytics, while PostgreSQL was primarily designed for OLTP. Regardless of whether storage is row- or columnar-based, the join algorithms are designed accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pushdown is not the same as row-group pruning
&lt;/h2&gt;

&lt;p&gt;The filter reaches &lt;code&gt;READ_PARQUET&lt;/code&gt;, but whether that avoids I/O depends on how the file is laid out. Inspect the &lt;code&gt;customer_ref&lt;/code&gt; statistics stored in three of this file's 17 row groups:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;
             &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;row_group_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;num_values&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;stats_min&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;min_customer_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;stats_max&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;max_customer_ref&lt;/span&gt;
             &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;parquet_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
               &lt;span class="s1"&gt;'az://lake/archive/sales_2021_minimized.parquet'&lt;/span&gt;
             &lt;span class="p"&gt;)&lt;/span&gt;
             &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;path_in_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'customer_ref'&lt;/span&gt;
               &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;row_group_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
             &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;row_group_id&lt;/span&gt;
           &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;row_group_id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;min_customer_ref&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;max_customer_ref&lt;/span&gt;
&lt;span class="c1"&gt;--------------+--------+------------------+------------------&lt;/span&gt;
 &lt;span class="mi"&gt;0&lt;/span&gt;            &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;122880&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;                &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt;
 &lt;span class="mi"&gt;1&lt;/span&gt;            &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;122880&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;                &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt;
 &lt;span class="mi"&gt;16&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;32858&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;                &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generator I used cycles through customer IDs, so every row group can include customer 43. DuckDB filters correctly, but min/max statistics cannot eliminate any row group. This is why a highly selective result does not automatically mean a cheap object-storage read.&lt;/p&gt;

&lt;p&gt;If customer-based lookups are important, sort by &lt;code&gt;customer_ref&lt;/code&gt; when writing each time partition. I tested an alternate export with &lt;code&gt;ORDER BY cust_id&lt;/code&gt;: its first row group covered customers 1–3,072, the next 3,072–6,148, and the last 49,179–50,000. A lookup for customer 43 can then reject 16 of 17 row groups based on metadata. For frequent point lookups with low-latency requirements, materializing the facts into an indexed PostgreSQL table may still be a better physical design than querying Parquet.&lt;/p&gt;

&lt;p&gt;The 39-row output of READ_PARQUET in the earlier plan shows the filter still ran at the row level — it saved decompression and hash probes, just not reads. With Parquet, there are two levels of pruning, and only metadata-level pruning avoids touching a row group.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this pattern does and does not guarantee
&lt;/h2&gt;

&lt;p&gt;Keeping identity out of immutable files does more than avoid rewrites. It keeps the authoritative personal record in the operational database, where a single governed update or deletion can take effect across every analytical query, rather than requiring the same action across many Parquet objects and downstream copies.&lt;/p&gt;

&lt;p&gt;But the boundary must be designed carefully:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data minimization matters. Do not put names, emails, phone numbers, addresses, or identifying free text in the fact file just because Parquet makes it convenient.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;customer_ref&lt;/code&gt; is pseudonymous while another system can map it back to a person. The sequential number in this lab is only for reproducibility; production should use a non-meaningful, opaque token and control every mapping.&lt;/li&gt;
&lt;li&gt;PostgreSQL becomes the authoritative location for identity lifecycle rules: who may read personal data, who may correct or erase it, what must be audited, and how retention is enforced.&lt;/li&gt;
&lt;li&gt;The query is not a permission boundary. Users who can query the base tables and files can still see the bridge key. In production, expose a reviewed view or function and restrict direct access to both sources.&lt;/li&gt;
&lt;li&gt;Deleting the live row does not retroactively remove it from historical backups. Erasure procedures must also define how replicas, exports, caches, logs, downstream results, backups, and restored environments are handled under the applicable retention policy.&lt;/li&gt;
&lt;li&gt;Some records may have legal retention requirements. Whether data must be erased, retained, quarantined, or anonymized is a policy and legal decision, not something &lt;code&gt;pg_duckdb&lt;/code&gt; decides.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This architecture gives governance a clear boundary: personal identity remains under PostgreSQL's operational controls, while the analytical tier receives only minimized, pseudonymous facts. It supports a GDPR program; it does not by itself prove compliance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keep immutable, minimized transaction facts in Parquet and personal identity in the governed operational database.&lt;/li&gt;
&lt;li&gt;Join them on an opaque reference only when a query needs both.&lt;/li&gt;
&lt;li&gt;The central pg_duckdb value is a DuckDB relational plan that spans heterogeneous Blob and PostgreSQL inputs, with an analytic-optimized engine, rather than coordinating separate local and foreign execution fragments.&lt;/li&gt;
&lt;li&gt;The full join hashes the smaller PostgreSQL input and streams the Parquet facts through it.&lt;/li&gt;
&lt;li&gt;pg_duckdb supplies base-table cardinality to DuckDB, while PostgreSQL still chooses the access path for delegated scans and can use its indexes.&lt;/li&gt;
&lt;li&gt;Customer-side filters can reduce both inputs, but Parquet row-group pruning depends on physical ordering and min/max statistics.&lt;/li&gt;
&lt;li&gt;For repeated selective lookups, sort within each time partition or materialize into an indexed table rather than assuming object storage behaves like an OLTP index.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One file is enough to demonstrate the join. A real archive grows into a tree of yearly or monthly files. In the next post, I will lay out that tree using Hive-style partitions and show DuckDB pruning out every file the query does not need.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>duckdb</category>
      <category>azure</category>
      <category>datalake</category>
    </item>
    <item>
      <title>Automate Postgres-to-Blob with durable workflow (pg_durable + pg_duckdb) in Azure Database for PostgreSQL</title>
      <dc:creator>Franck Pachot</dc:creator>
      <pubDate>Tue, 13 Oct 2026 09:07:59 +0000</pubDate>
      <link>https://dev.to/franckpachot/automate-the-postgres-to-blob-archive-with-a-durable-workflow-pgdurable-pgduckdb-2edc</link>
      <guid>https://dev.to/franckpachot/automate-the-postgres-to-blob-archive-with-a-durable-workflow-pgdurable-pgduckdb-2edc</guid>
      <description>&lt;p&gt;In the &lt;a href="https://dev.to/franckpachot/archive-cold-postgres-data-to-azure-blob-and-keep-querying-it-with-pgduckdb-8ej"&gt;archive post&lt;/a&gt; I moved cold rows out of a Postgres table into Parquet on Azure Blob and kept querying them. The steps were manual: export to Blob, verify the file, delete the rows, record what happened. If the process dies after the export but before the delete, you're stuck — re-run and you re-export (wasteful), skip ahead and you might delete rows that were never safely written (dangerous). That partial-failure gap is exactly what a durable workflow closes.&lt;/p&gt;

&lt;p&gt;Azure Database for PostgreSQL ships another open-source Microsoft extension that fits here: &lt;a href="https://github.com/microsoft/pg_durable" rel="noopener noreferrer"&gt;&lt;code&gt;pg_durable&lt;/code&gt;&lt;/a&gt;. It runs checkpointed, crash-safe workflows &lt;em&gt;inside&lt;/em&gt; Postgres — define a graph of SQL steps, and the engine persists progress between them. If the database restarts or a step fails, execution resumes from the last completed step instead of starting over. No cron-plus-status-table-plus-worker scaffolding. This post turns the archive into one durable, schedulable workflow, with &lt;code&gt;pg_duckdb&lt;/code&gt; doing the write to Blob.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling pg_durable
&lt;/h2&gt;

&lt;p&gt;Like &lt;code&gt;pg_duckdb&lt;/code&gt;, &lt;code&gt;pg_durable&lt;/code&gt; loads through &lt;code&gt;shared_preload_libraries&lt;/code&gt;, so it needs allow-listing and a restart:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add &lt;code&gt;pg_durable&lt;/code&gt; to &lt;code&gt;azure.extensions&lt;/code&gt; (the allow-list).&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;pg_durable&lt;/code&gt; to &lt;code&gt;shared_preload_libraries&lt;/code&gt; (append to the current value, next to &lt;code&gt;pg_duckdb&lt;/code&gt;) and restart the server.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then create both extensions in the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pg_duckdb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pg_durable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_azure_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;blob connection string&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pg_durable&lt;/code&gt; runs its steps in a background worker. Right after a restart that worker takes a moment to come up. The first &lt;code&gt;df.start()&lt;/code&gt; can return &lt;code&gt;pg_durable background worker not yet initialized — try again in a moment&lt;/code&gt;. Wait a few seconds and retry. A normal (non-superuser) login also needs a one-time grant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grant_usage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick smoke test confirms the engine runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT 1 AS ok'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

 &lt;span class="k"&gt;start&lt;/span&gt;
&lt;span class="c1"&gt;----------&lt;/span&gt;
 &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="n"&gt;a27f7d&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'50a27f7d'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="c1"&gt;-----------&lt;/span&gt;
 &lt;span class="n"&gt;completed&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'50a27f7d'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

             &lt;span class="k"&gt;result&lt;/span&gt;
&lt;span class="c1"&gt;--------------------------------&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;"rows"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nv"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="nv"&gt;"row_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow status, errors, and results are persisted for each step and you can query them with &lt;code&gt;pg_durable&lt;/code&gt; functions and tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data to archive
&lt;/h2&gt;

&lt;p&gt;A small &lt;code&gt;orders&lt;/code&gt; table spanning three years — the 2023 rows are the cold slice we'll move out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2023-01-01'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;1095&lt;/span&gt;&lt;span class="p"&gt;))::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;              &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;archive_log&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="nb"&gt;serial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;archived_year&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;n_rows&lt;/span&gt;        &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nb"&gt;blob&lt;/span&gt;          &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;at&lt;/span&gt;            &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the current data in this table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;-- total number of rows&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="k"&gt;count&lt;/span&gt;
&lt;span class="c1"&gt;-------&lt;/span&gt;
  &lt;span class="mi"&gt;1000&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;-- the cold slice&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; 
           &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="k"&gt;count&lt;/span&gt;
&lt;span class="c1"&gt;-------&lt;/span&gt;
   &lt;span class="mi"&gt;364&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have a reproducible test case where I want to archive orders from before 2024. I'll create a one-time workflow for that and let it run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The durable workflow
&lt;/h2&gt;

&lt;p&gt;The whole archive is one &lt;code&gt;df.start()&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;steps are chained with &lt;code&gt;~&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;step's result is captured into a variable with &lt;code&gt;|=&amp;gt; 'name'&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;next step can reuse the result as &lt;code&gt;$name&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;if-then-else is declared with &lt;code&gt;()&lt;/code&gt;, &lt;code&gt;?&amp;gt;&lt;/code&gt;, &lt;code&gt;!&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="c1"&gt;-- 1. count the cold slice, capture it as $n&lt;/span&gt;
     &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Step1&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;
     &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Step1&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="o"&gt;|=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'n'&lt;/span&gt;
  &lt;span class="c1"&gt;-- 2. write those rows to Blob as Parquet, through the DuckDB engine&lt;/span&gt;
  &lt;span class="o"&gt;~&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Step2&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt; 
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;DuckDB&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;
           &lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
             &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pgduckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;
             &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'az://lake/archive/orders_2023.parquet'&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;parquet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;DuckDB&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Step2&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;
  &lt;span class="c1"&gt;-- 3. verify the file reads back, capture the row count as $v&lt;/span&gt;
  &lt;span class="o"&gt;~&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Step3&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
        &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/archive/orders_2023.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Step3&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="o"&gt;|=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'v'&lt;/span&gt;
  &lt;span class="c1"&gt;-- 4. delete only if the archive count matches&lt;/span&gt;
  &lt;span class="o"&gt;~&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;Check&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;archive_complete&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;Check&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;
       &lt;span class="o"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Del&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;  &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Del&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;
       &lt;span class="o"&gt;!&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Fail&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="k"&gt;BEGIN&lt;/span&gt;
                   &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'archive incomplete: expected % rows, file has %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                 &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Fail&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;
     &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;-- 5. record what happened&lt;/span&gt;
  &lt;span class="o"&gt;~&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Step5&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;
        &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;archive_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archived_year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2023&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'az://lake/archive/orders_2023.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
     &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Step5&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'durable-archive'&lt;/span&gt;   &lt;span class="c1"&gt;-- a label to find the run by&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things about the write step (step 2), both carried over from the archive post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The blob write goes through &lt;code&gt;duckdb.raw_query()&lt;/code&gt;, not plain &lt;code&gt;COPY TO 'az://…'&lt;/code&gt; so that DuckDB authorizes the write according to its own secret and allow-list — the object-storage controls that PostgreSQL's server-file gate knows nothing about and would otherwise reject.&lt;/li&gt;
&lt;li&gt;Inside &lt;code&gt;raw_query&lt;/code&gt; you're in DuckDB's catalog, so the table is &lt;code&gt;pgduckdb.public.orders&lt;/code&gt;, not a bare &lt;code&gt;orders&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The condition is a safety check to run the delete only when the count matches, or fail otherwise.&lt;/p&gt;

&lt;p&gt;And a word on the quoting: the steps are SQL statements declared as text arguments for &lt;code&gt;df.start()&lt;/code&gt; and themselves include nested text. This would require multiple levels of double quotes in standard SQL. It is solved easily with PostgreSQL dollar quoting (&lt;code&gt;$$&lt;/code&gt; with an optional label inside).&lt;/p&gt;

&lt;h2&gt;
  
  
  Watching it run
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;df.start()&lt;/code&gt; returns immediately with an instance &lt;code&gt;id&lt;/code&gt;. The worker executes the steps. You can check progress directly with the &lt;code&gt;id&lt;/code&gt; or getting the last &lt;code&gt;id&lt;/code&gt; with your label:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;instance_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;execution_count&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;list_instances&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
           &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;instance_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instances&lt;/span&gt;
                                &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'durable-archive'&lt;/span&gt;
                                &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;instance_id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;      &lt;span class="n"&gt;label&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="n"&gt;status&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;execution_count&lt;/span&gt;
&lt;span class="c1"&gt;-------------+-----------------+-----------+-----------------&lt;/span&gt;
 &lt;span class="n"&gt;b9b24177&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;durable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;archive&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;               &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The steps are visible with &lt;code&gt;df.explain()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;explain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'b9b24177'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                                &lt;span class="k"&gt;explain&lt;/span&gt;
&lt;span class="c1"&gt;-----------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;b9b24177&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;durable&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                 &lt;span class="o"&gt;+&lt;/span&gt;
 &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="err"&gt;✓&lt;/span&gt; &lt;span class="n"&gt;Completed&lt;/span&gt;                                                &lt;span class="o"&gt;+&lt;/span&gt;
 &lt;span class="k"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;"rows"&lt;/span&gt;&lt;span class="p"&gt;:[],&lt;/span&gt;&lt;span class="nv"&gt;"row_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;                                  &lt;span class="o"&gt;+&lt;/span&gt;
                                                                      &lt;span class="o"&gt;+&lt;/span&gt;
 &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                                                 &lt;span class="o"&gt;+&lt;/span&gt;
         &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="n"&gt;WHER&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="o"&gt;|=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'n'&lt;/span&gt; &lt;span class="err"&gt;✓&lt;/span&gt;           &lt;span class="o"&gt;+&lt;/span&gt;
 &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                                               &lt;span class="o"&gt;+&lt;/span&gt;
         &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;                                     &lt;span class="o"&gt;+&lt;/span&gt;
         &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Du&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="err"&gt;✓&lt;/span&gt;                                                     &lt;span class="o"&gt;+&lt;/span&gt;
 &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                                               &lt;span class="o"&gt;+&lt;/span&gt;
         &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;                                         &lt;span class="o"&gt;+&lt;/span&gt;
         &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="err"&gt;✓&lt;/span&gt;                                                 &lt;span class="o"&gt;+&lt;/span&gt;
 &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="err"&gt;✓&lt;/span&gt;                                                               &lt;span class="o"&gt;+&lt;/span&gt;
     &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                                     &lt;span class="o"&gt;+&lt;/span&gt;
       &lt;span class="err"&gt;└─&lt;/span&gt;&lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;archive_complete&lt;/span&gt;  &lt;span class="err"&gt;✓&lt;/span&gt;                  &lt;span class="o"&gt;+&lt;/span&gt;
     &lt;span class="err"&gt;✓&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                                          &lt;span class="o"&gt;+&lt;/span&gt;
       &lt;span class="err"&gt;└─&lt;/span&gt;&lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2... ✓    +
     ✗ else:                                                          +
       └─SQL:  DO $do$ BEGIN                                          +
                    RAISE EXCEPT... ⊘                                 +
 → SQL:                                                               +
         INSERT INTO archive_log(archived_year... ✓
(1 row)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step is a node you can inspect — this is the checkpoint record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;node_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;
           &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;instance_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instances&lt;/span&gt;
                                &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'durable-archive'&lt;/span&gt;
                                &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;updated_at&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;node_type&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="n"&gt;status&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt;                                             &lt;span class="n"&gt;query&lt;/span&gt;
&lt;span class="c1"&gt;-----------+-----------+------------------------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="k"&gt;SQL&lt;/span&gt;       &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="k"&gt;BEGIN&lt;/span&gt;                                                                                &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;                    &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'archive incomplete: expected % rows, file has %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;                  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                                                                    &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="k"&gt;SQL&lt;/span&gt;       &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                                                                                               &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;                 &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="k"&gt;SQL&lt;/span&gt;       &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                                                                                               &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;                                                              &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;DuckDB&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;                                                                              &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;            &lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;                                                                             &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;              &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;                                              &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;              &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pgduckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;                                                      &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;              &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;                                             &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'az://lake/archive/orders_2023.parquet'&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;parquet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;DuckDB&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;                                                                              &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="p"&gt;)&lt;/span&gt;                                                                                     &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="k"&gt;THEN&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="k"&gt;SQL&lt;/span&gt;       &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                                                                                               &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;                                                                  &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/archive/orders_2023.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                            &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="k"&gt;THEN&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="k"&gt;SQL&lt;/span&gt;       &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;archive_complete&lt;/span&gt;
 &lt;span class="k"&gt;SQL&lt;/span&gt;       &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;   &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;
 &lt;span class="n"&gt;IF&lt;/span&gt;        &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;"condition_node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;"3149a512"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="k"&gt;THEN&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="k"&gt;SQL&lt;/span&gt;       &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                                                                                               &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;archive_log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archived_year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                  &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2023&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'az://lake/archive/orders_2023.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                            &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="o"&gt;|&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt;
 &lt;span class="k"&gt;THEN&lt;/span&gt;      &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;completed&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gate adds nodes: the &lt;code&gt;SELECT $v = $n AS archive_complete&lt;/code&gt; check, its &lt;code&gt;IF&lt;/code&gt; node, and both branches — the &lt;code&gt;DELETE&lt;/code&gt; ran and is &lt;code&gt;completed&lt;/code&gt;, while the &lt;code&gt;DO ... RAISE EXCEPTION&lt;/code&gt; branch stayed pending because it was never taken.&lt;/p&gt;

&lt;h2&gt;
  
  
  It worked, end to end
&lt;/h2&gt;

&lt;p&gt;As expected by the completed workflow, the archived file is in Blob that I can query though &lt;code&gt;pg_duckdb&lt;/code&gt; functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/archive/orders_2023.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="k"&gt;count&lt;/span&gt;
&lt;span class="c1"&gt;-------&lt;/span&gt;
   &lt;span class="mi"&gt;364&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cold rows are gone from Postgres:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="k"&gt;count&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;    &lt;span class="k"&gt;min&lt;/span&gt;
&lt;span class="c1"&gt;-------+------------&lt;/span&gt;
   &lt;span class="mi"&gt;636&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2024&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The audit row is written in my custom table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;archived_year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;blob&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;archive_log&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;at&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;archived_year&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n_rows&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;                 &lt;span class="nb"&gt;blob&lt;/span&gt;
&lt;span class="c1"&gt;---------------+--------+---------------------------------------&lt;/span&gt;
          &lt;span class="mi"&gt;2023&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;    &lt;span class="mi"&gt;364&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="n"&gt;lake&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;archive&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;orders_2023&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parquet&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we have seen in the previous post, history is still one query away, live rows unioned with the archived file, because &lt;code&gt;pg_duckdb&lt;/code&gt; can run queries on heterogenous sources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;yr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;yr&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
             &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
             &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'order_date'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;
             &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/archive/orders_2023.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
           &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;yr&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;yr&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;yr&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="c1"&gt;------+-----&lt;/span&gt;
 &lt;span class="mi"&gt;2023&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;364&lt;/span&gt;
 &lt;span class="mi"&gt;2024&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;324&lt;/span&gt;
 &lt;span class="mi"&gt;2025&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;312&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2023 comes from the archived Blob file and 2024 and 2025 from the live Postgres table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why durable, not just a cron job
&lt;/h2&gt;

&lt;p&gt;Steps 2→3→4 are export, verify, &lt;em&gt;then&lt;/em&gt; delete — and because &lt;code&gt;pg_durable&lt;/code&gt; checkpoints after each step, a crash between them is safe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Die after the export but before the delete, and the run resumes at the verify and delete steps on restart. The export is not repeated. The delete is not skipped.&lt;/li&gt;
&lt;li&gt;A failed step stops the workflow with its state intact in &lt;code&gt;df.nodes&lt;/code&gt;, so you can see exactly where it stopped and why — no guessing, no half-done cleanup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a plain &lt;code&gt;pg_cron&lt;/code&gt; + PL/pgSQL procedure you'd be rebuilding this yourself: a status table, retry counters, and a way to tell "exported but not yet deleted" apart from "nothing done yet." &lt;code&gt;pg_durable&lt;/code&gt; keeps that bookkeeping inside Postgres.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make it nightly
&lt;/h2&gt;

&lt;p&gt;Wrap the same workflow in an eternal loop (&lt;code&gt;@&amp;gt;&lt;/code&gt;) that waits on a cron schedule, and the archive runs on its own, forever, surviving restarts. Point the date window at a rolling boundary instead of a fixed year:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;  
  &lt;span class="o"&gt;@&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;  
    &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Step0&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;  
       &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date_trunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'year'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'1 year'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt;  
    &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Step0&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="o"&gt;|=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'cutoff'&lt;/span&gt;  
    &lt;span class="o"&gt;~&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Step1&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;  
         &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;cutoff&lt;/span&gt;  
       &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;Step1&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="o"&gt;|=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'n'&lt;/span&gt;   
    &lt;span class="c1"&gt;-- copy / verify &lt;/span&gt;
    &lt;span class="o"&gt;~&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;View&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;  
         &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;orders_all&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;  
           &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;  
           &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;cutoff&lt;/span&gt;          &lt;span class="c1"&gt;-- exclude rows about to be deleted  &lt;/span&gt;
           &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;  
           &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;  
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/archive/orders_*.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
   &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;View&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;  
    &lt;span class="c1"&gt;-- the same ( check ?&amp;gt; delete !&amp;gt; fail ) gate, then log — all referencing $cutoff&lt;/span&gt;
    &lt;span class="o"&gt;~&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wait_for_schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'0 2 * * *'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
  &lt;span class="p"&gt;),&lt;/span&gt;  
  &lt;span class="s1"&gt;'nightly-archive'&lt;/span&gt;  
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I added a step to (re)build a view that reads from the archives and adds the operational data. I'll explain how to query a partitioned data lake in a future post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pg_durable&lt;/code&gt; is another open-source extension on Azure Database for PostgreSQL, enabled like &lt;code&gt;pg_duckdb&lt;/code&gt; (&lt;code&gt;azure.extensions&lt;/code&gt; + &lt;code&gt;shared_preload_libraries&lt;/code&gt; + restart), usable in your database after &lt;code&gt;CREATE EXTENSION pg_durable&lt;/code&gt;, and by your user after &lt;code&gt;df.grant_usage(current_user)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The archive becomes one &lt;code&gt;df.start()&lt;/code&gt; graph: count → write to Blob (&lt;code&gt;duckdb.raw_query&lt;/code&gt;) → verify → delete → log, checkpointed between steps.&lt;/li&gt;
&lt;li&gt;Durability closes the export-then-delete gap: a crash resumes at the next uncompleted step instead of re-exporting or deleting unsafely.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@&amp;gt;&lt;/code&gt; + &lt;code&gt;df.wait_for_schedule('0 2 * * *')&lt;/code&gt; turns it into a nightly job that survives restarts — no external scheduler, no status table.&lt;/li&gt;
&lt;li&gt;Everything is plain SQL with additional operators to define the workflow.&lt;/li&gt;
&lt;li&gt;Each step is a transaction, so intermediate results are durable — the workflow continues automatically after a failover.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can visualize the workflow from VS Code with the &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-ossdata.vscode-pgsql" rel="noopener noreferrer"&gt;PostgreSQL extension by Microsoft&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>duckdb</category>
      <category>azure</category>
      <category>pgdurable</category>
    </item>
    <item>
      <title>Archive cold Postgres data to Azure Blob and keep querying it with pg_duckdb</title>
      <dc:creator>Franck Pachot</dc:creator>
      <pubDate>Sun, 11 Oct 2026 17:16:16 +0000</pubDate>
      <link>https://dev.to/franckpachot/archive-cold-postgres-data-to-azure-blob-and-keep-querying-it-with-pgduckdb-8ej</link>
      <guid>https://dev.to/franckpachot/archive-cold-postgres-data-to-azure-blob-and-keep-querying-it-with-pgduckdb-8ej</guid>
      <description>&lt;p&gt;A Postgres table that only grows is a familiar problem: queries touch recent data, but old rows still consume storage and slow maintenance. With &lt;code&gt;pg_duckdb&lt;/code&gt;, you can move cold data to Parquet in Azure Blob Storage and still query it — on its own and joined back to the live table — from the same connection. Here's the full cycle for a &lt;code&gt;sales&lt;/code&gt; table: 10M rows over five years, with 2021 archived out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup (same table as the previous post)
&lt;/h2&gt;

&lt;p&gt;I'll use the same data as in the previous post &lt;a href="https://dev.to/franckpachot/duckdb-analytics-on-your-postgresql-tables-in-azure-postgresql-9m8/"&gt;DuckDB analytics on your PostgreSQL tables in Azure PostgreSQL&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;force_execution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sale_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2021-01-01'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;1826&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;                       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cust_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;prod_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'US'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'CH'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'FR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'DE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'UK'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'JP'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'BR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'IN'&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Consumer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'SMB'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Enterprise'&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;          &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;))::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10000000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pg_duckdb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_azure_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&amp;lt;blob connection string&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I set &lt;code&gt;force_execution = false&lt;/code&gt;, the default, in case you activated it when reproducing the previous post. The table needs to be built on the PostgreSQL executor. If DuckDB is enforced, it issues a warning: &lt;code&gt;Binder Error: +(DATE, BIGINT)&lt;/code&gt; on this &lt;code&gt;generate_series&lt;/code&gt; DDL and defaults back to Postgres.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing to Blob from inside Azure Database for PostgreSQL
&lt;/h2&gt;

&lt;p&gt;One point up front, because it shapes the workflow. &lt;code&gt;pg_duckdb&lt;/code&gt; &lt;em&gt;can&lt;/em&gt; write Parquet to Azure Blob from inside the database — but on Azure Database for PostgreSQL the plain &lt;code&gt;COPY … TO 'az://…'&lt;/code&gt; syntax is refused:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'az://lake/archive/sales_2021.parquet'&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;parquet&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;--  ERROR: relative path not allowed for COPY to/from a file in Azure Database For PostgreSQL&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On self-managed PostgreSQL, the &lt;code&gt;pg_duckdb&lt;/code&gt; extension intercepts &lt;code&gt;COPY ... TO 'az://...'&lt;/code&gt; and runs it in DuckDB for you. However, Azure ensures that extensions cannot bypass PostgreSQL security checks. It considers any path without a leading &lt;code&gt;/&lt;/code&gt; as "relative", causing the &lt;code&gt;az://&lt;/code&gt; URL to be rejected. This occurs before the server requires superuser privileges or &lt;code&gt;pg_write_server_files&lt;/code&gt; permissions for server-side file operations. Azure allows PostgreSQL extensions but limits their ability to bypass the managed environment's security controls.&lt;/p&gt;

&lt;p&gt;The way through is &lt;code&gt;duckdb.raw_query&lt;/code&gt;, which runs the &lt;code&gt;COPY&lt;/code&gt; inside the embedded DuckDB engine. This isn't a way around the platform's guard — it's a write to a different resource, governed by a different control: the Blob write is governed instead by DuckDB's own secret and allow-list (&lt;code&gt;duckdb.allowed_directories&lt;/code&gt;, here &lt;code&gt;az://&lt;/code&gt;), so the write lands through the layer that actually owns that resource. That's the in-database export used below. If you'd rather not write from the database at all, a client-side export works too — see the alternative at the end of Step 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Identify the cold slice
&lt;/h2&gt;

&lt;p&gt;I'll consider sales prior to 2022 as old data to be archived:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;count&lt;/span&gt;
&lt;span class="c1"&gt;----------&lt;/span&gt;
 &lt;span class="mi"&gt;10000000&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; 
           &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-01'&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;count&lt;/span&gt;
&lt;span class="c1"&gt;---------&lt;/span&gt;
 &lt;span class="mi"&gt;1998938&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will retain just under 2 million rows from 2021 in the operational database and archive the older data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Export to Parquet in Blob (in-database)
&lt;/h2&gt;

&lt;p&gt;Hand the &lt;code&gt;COPY&lt;/code&gt; to the DuckDB engine with &lt;code&gt;duckdb.raw_query()&lt;/code&gt;, writing straight to Blob. Keep types faithful — the source &lt;code&gt;amount&lt;/code&gt; is already &lt;code&gt;numeric(10,2)&lt;/code&gt;, so DuckDB writes it as &lt;code&gt;DECIMAL(10,2)&lt;/code&gt; and reads it back natively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$$&lt;/span&gt;
             &lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
               &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;sale_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cust_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prod_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
               &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pgduckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;
               &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-01'&lt;/span&gt;
             &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'az://lake/archive/sales_2021.parquet'&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;parquet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;raw_query&lt;/span&gt;
&lt;span class="c1"&gt;-----------&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four things to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Qualify the table as &lt;code&gt;pgduckdb.public.sales&lt;/code&gt;, not &lt;code&gt;sales&lt;/code&gt; because &lt;code&gt;raw_query&lt;/code&gt; runs inside the DuckDB engine, so it sees DuckDB's catalog, not PostgreSQL's search path. &lt;code&gt;pg_duckdb&lt;/code&gt; mounts your Postgres tables under the &lt;code&gt;pgduckdb.public.*&lt;/code&gt; catalog. A bare &lt;code&gt;sales&lt;/code&gt; fails with &lt;code&gt;Catalog Error: Table with name sales does not exist&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;duckdb.query()&lt;/code&gt; won't carry this — it only accepts a single &lt;code&gt;SELECT&lt;/code&gt;, so the &lt;code&gt;COPY&lt;/code&gt; must go through &lt;code&gt;raw_query()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The write target must be under the &lt;code&gt;az://&lt;/code&gt; allow-list (a local path fails with &lt;code&gt;LocalFileSystem has been disabled by configuration&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;If the target blob already exists and was created by something other than DuckDB (an &lt;code&gt;az storage blob upload&lt;/code&gt;, say), the write can fail with &lt;code&gt;IO Error: AzureStorageFileSystem … 'InvalidBlobType'&lt;/code&gt;. DuckDB happily overwrites files it wrote itself, but not a blob another tool created with an incompatible type. Write to a fresh path, or delete the stale blob first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is a &lt;code&gt;sales_2021.parquet&lt;/code&gt; of about 17 MB for these ~2M rows, already in Blob — no upload step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fritz9nz8hbelxl6htvnr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fritz9nz8hbelxl6htvnr.png" alt=" " width="799" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwv6xgp12j70lhe180yi6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwv6xgp12j70lhe180yi6.png" alt=" " width="757" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Alternative — client-side export
&lt;/h3&gt;

&lt;p&gt;If you can't or don't want to write from the database, pull the rows out and write the file client-side, then upload it. &lt;/p&gt;

&lt;p&gt;The DuckDB CLI can read Postgres directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- in the duckdb CLI&lt;/span&gt;
&lt;span class="n"&gt;ATTACH&lt;/span&gt; &lt;span class="s1"&gt;'host=… dbname=… user=… password=… sslmode=require'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;pg&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;sale_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cust_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prod_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-01'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'sales_2021.parquet'&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;parquet&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting file can be uploaded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az storage blob upload &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--account-name&lt;/span&gt; &amp;lt;account&amp;gt; &lt;span class="nt"&gt;--account-key&lt;/span&gt; &amp;lt;key&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; lake &lt;span class="nt"&gt;-f&lt;/span&gt; sales_2021.parquet &lt;span class="nt"&gt;-n&lt;/span&gt; archive/sales_2021.parquet &lt;span class="nt"&gt;--overwrite&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This method is a possible alternative, but using PostgreSQL with a DuckDB COPY command offers the best security because it keeps the data within the cloud services protected by their native authentication methods.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Read it back through pg_duckdb
&lt;/h2&gt;

&lt;p&gt;You can read the files from PostgreSQL to verify their content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/archive/sales_2021.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;count&lt;/span&gt;
&lt;span class="c1"&gt;---------&lt;/span&gt;
 &lt;span class="mi"&gt;1998938&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 2021 data is queryable straight from Blob. Note that the count(*) didn't read all rows, as the row count is available in the Parquet metadata footer and DuckDB gets the count from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Delete the cold rows from Postgres
&lt;/h2&gt;

&lt;p&gt;Now that it's safely in Blob and verified, reclaim the space for new inserts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; 
           &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-01'&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="mi"&gt;1998938&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;count&lt;/span&gt;
&lt;span class="c1"&gt;---------&lt;/span&gt;
 &lt;span class="mi"&gt;8001062&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If lifecycle management wasn't in place from the start and you've deleted a large amount of data, you may need to reorg the table to actually shrink it on disk (&lt;code&gt;pg_repack&lt;/code&gt; and &lt;code&gt;pg_squeeze&lt;/code&gt; are available on Azure Database for PostgreSQL). Plain DELETE only marks rows as dead — it doesn't free disk space. A subsequent VACUUM reclaims that space, but it doesn't return it to the operating system: the freed heap space becomes available for any new row inserted into that table (tracked via the Free Space Map). In B-Tree indexes, reclaimed space is only reusable by new index entries whose key value falls into the same page/block — because B-Tree pages are key-ordered, not by arbitrary new values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — Keep querying history, concatenated with live data
&lt;/h2&gt;

&lt;p&gt;The 2021 rows are gone from Postgres, but they are accessible from Parquet, and one query can span both. Here is an aggregate of the revenue per year, with hot rows from Postgres and old ones from Blob:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;yr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="c1"&gt;-- Operational data in PostgreSQL&lt;/span&gt;
             &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;yr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
             &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
             &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-01'&lt;/span&gt;
             &lt;span class="c1"&gt;-- Heterogeneous Federation &lt;/span&gt;
             &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
             &lt;span class="c1"&gt;-- Archived data in Blob&lt;/span&gt;
             &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sale_date'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
             &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/archive/sales_2021.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;   
           &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
           &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;yr&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;yr&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;yr&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt;    &lt;span class="n"&gt;n&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt;   &lt;span class="n"&gt;revenue&lt;/span&gt;
&lt;span class="c1"&gt;------+---------+--------------&lt;/span&gt;
 &lt;span class="mi"&gt;2021&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1998938&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;508738403&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;
 &lt;span class="mi"&gt;2022&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1998897&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;508731504&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;
 &lt;span class="mi"&gt;2023&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1998897&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;508723449&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;
 &lt;span class="mi"&gt;2024&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;2004372&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;510107354&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;
 &lt;span class="mi"&gt;2025&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1998896&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;508699290&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The execution plan shows one &lt;code&gt;DuckDBScan&lt;/code&gt; with a &lt;code&gt;UNION&lt;/code&gt; over the Postgres scan and the Blob read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;yr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;revenue&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
             &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;yr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
             &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
             &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2022-01-01'&lt;/span&gt;
             &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
             &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sale_date'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'amount'&lt;/span&gt;&lt;span class="p"&gt;])::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
             &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/archive/sales_2021.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
           &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
           &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;yr&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;yr&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                   &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;-------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;Custom&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DuckDBScan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;002&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;003&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;DuckDB&lt;/span&gt; &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="n"&gt;Plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

 &lt;span class="err"&gt;┌─────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌───────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;    &lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;Profiling&lt;/span&gt; &lt;span class="n"&gt;Information&lt;/span&gt;    &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└───────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌────────────────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌──────────────────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;               &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;              &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└──────────────────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└────────────────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌───────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;QUERY&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="n"&gt;EXPLAIN_ANALYZE&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="n"&gt;ORDER_BY&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yr&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="n"&gt;HASH_GROUP_BY&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;Groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Aggregates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;count_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="n"&gt;yr&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;amount&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="k"&gt;UNION&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;├──────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;              &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;││&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;││&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="n"&gt;yr&lt;/span&gt;            &lt;span class="err"&gt;││&lt;/span&gt;          &lt;span class="k"&gt;extract&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;amount&lt;/span&gt;          &lt;span class="err"&gt;││&lt;/span&gt;             &lt;span class="n"&gt;r&lt;/span&gt;             &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;││&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;062&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;      &lt;span class="err"&gt;││&lt;/span&gt;       &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;998&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;938&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;││&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;TABLE_SCAN&lt;/span&gt;        &lt;span class="err"&gt;││&lt;/span&gt;         &lt;span class="n"&gt;TABLE_SCAN&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;││&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="k"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;       &lt;span class="err"&gt;││&lt;/span&gt;         &lt;span class="k"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;││&lt;/span&gt;        &lt;span class="n"&gt;READ_PARQUET&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="err"&gt;││&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;sale_date&lt;/span&gt;         &lt;span class="err"&gt;││&lt;/span&gt;        &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;amount&lt;/span&gt;          &lt;span class="err"&gt;││&lt;/span&gt;         &lt;span class="n"&gt;sale_date&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="n"&gt;Filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="err"&gt;││&lt;/span&gt;           &lt;span class="n"&gt;amount&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;  &lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="s1"&gt;'2022-01-01'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;││&lt;/span&gt;    &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="n"&gt;Files&lt;/span&gt; &lt;span class="k"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;DATE&lt;/span&gt;           &lt;span class="err"&gt;││&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;││&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;062&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;      &lt;span class="err"&gt;││&lt;/span&gt;       &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;998&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;938&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;││&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└───────────────────────────┘└───────────────────────────┘&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;446&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;973&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
 &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;224&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;008&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The left leaf reads the live table (with the &lt;code&gt;sale_date &amp;gt;= 2022&lt;/code&gt; filter pushed into the scan). The right leaf reads the archive from Blob. DuckDB unions and aggregates them. Note you don't need &lt;code&gt;SET duckdb.force_execution = true&lt;/code&gt; — a query that references &lt;code&gt;read_parquet&lt;/code&gt; goes to DuckDB automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it a routine
&lt;/h2&gt;

&lt;p&gt;If you partition the table by time (&lt;code&gt;pg_partman&lt;/code&gt; is available on Azure Database for PostgreSQL), archiving a month becomes: export that partition, upload, &lt;code&gt;DROP&lt;/code&gt; the partition. It is far cheaper than a big &lt;code&gt;DELETE&lt;/code&gt; + &lt;code&gt;VACUUM&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Lay the archive out by period (&lt;code&gt;archive/year=2021/…&lt;/code&gt;) so you can read one period or glob a range — I will cover that in a future post.&lt;/p&gt;

&lt;p&gt;Keep the export faithful — the source &lt;code&gt;amount&lt;/code&gt; is &lt;code&gt;numeric(10,2)&lt;/code&gt;, so DuckDB writes and reads it as &lt;code&gt;DECIMAL(10,2)&lt;/code&gt; natively (a bare numeric falls back to the Postgres executor).&lt;/p&gt;

&lt;p&gt;I will show a fully automated information lifecycle management with &lt;code&gt;pg_durable&lt;/code&gt; in a future blog post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pg_duckdb&lt;/code&gt; on Azure Database for PostgreSQL writes Parquet to Blob via &lt;code&gt;duckdb.raw_query($$ COPY (…) TO 'az://…' $$)&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;read_parquet('az://…')&lt;/code&gt; makes archived data queryable again.  &lt;code&gt;UNION&lt;/code&gt;/join it with the live table in one statement.&lt;/li&gt;
&lt;li&gt;You reclaim Postgres storage without losing query access to the history.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the next post, we will see how to join a live Postgres table with reference data in Blob.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>duckdb</category>
      <category>azure</category>
      <category>datalake</category>
    </item>
    <item>
      <title>DuckDB analytics on your PostgreSQL tables in Azure PostgreSQL</title>
      <dc:creator>Franck Pachot</dc:creator>
      <pubDate>Fri, 09 Oct 2026 19:07:22 +0000</pubDate>
      <link>https://dev.to/franckpachot/duckdb-analytics-on-your-postgresql-tables-in-azure-postgresql-9m8</link>
      <guid>https://dev.to/franckpachot/duckdb-analytics-on-your-postgresql-tables-in-azure-postgresql-9m8</guid>
      <description>&lt;p&gt;In the &lt;a href="https://dev.to/franckpachot/the-full-pgduckdb-extension-on-azure-database-for-postgresql-2hh7"&gt;previous post&lt;/a&gt;, we discussed how &lt;code&gt;pg_duckdb&lt;/code&gt; on Azure offers more than just reading Parquet files. It integrates DuckDB's columnar, vectorized engine directly into PostgreSQL. A practical first step requires no data lake: simply point DuckDB at your existing tables and run analytical queries more quickly — at times. This post guides you through enabling this on Azure Database for PostgreSQL (Flexible Server), highlights its advantages, and notes when it might not be as effective. It's a choice driven by your knowledge of the types of analytics you run, not a query planner decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable pg_duckdb on Azure Database for PostgreSQL
&lt;/h2&gt;

&lt;p&gt;pg_duckdb loads through &lt;code&gt;shared_preload_libraries&lt;/code&gt;, so it needs two server parameters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add &lt;code&gt;pg_duckdb&lt;/code&gt; to &lt;code&gt;azure.extensions&lt;/code&gt; (the allow-list).&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;pg_duckdb&lt;/code&gt; to &lt;code&gt;shared_preload_libraries&lt;/code&gt; (append to the current value) and restart the server.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then, as your normal login:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;pg_duckdb&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the open-source DuckDB extension for PostgreSQL and you have access to all functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  A table to analyze
&lt;/h2&gt;

&lt;p&gt;I created a table with ten million sales rows spanning over five years. Note that &lt;code&gt;amount&lt;/code&gt; is &lt;code&gt;numeric(10,2)&lt;/code&gt; with precision, and that detail matters. I come back to it at the end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;NOTICE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="nv"&gt;"sales"&lt;/span&gt; &lt;span class="n"&gt;does&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;exist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skipping&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;force_execution&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;force_execution&lt;/span&gt;
&lt;span class="c1"&gt;------------------------&lt;/span&gt;
 &lt;span class="k"&gt;off&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
           &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sale_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2021-01-01'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;1826&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;                        &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;sale_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                               &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cust_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;prod_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                  &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="n"&gt;S&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'US'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'CH'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'FR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'DE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'UK'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'JP'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'BR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'IN'&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Consumer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'SMB'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'Enterprise'&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;           &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                  &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;))::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;                              &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10000000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;10000000&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;VACUUM&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;VACUUM&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Turning DuckDB on
&lt;/h2&gt;

&lt;p&gt;For a query that touches only regular PostgreSQL tables, &lt;code&gt;pg_duckdb&lt;/code&gt; does nothing until you ask. The switch is a session setting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;force_execution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With it on, &lt;code&gt;pg_duckdb&lt;/code&gt; hands the query to DuckDB when it can, and you see a single &lt;code&gt;Custom Scan (DuckDBScan)&lt;/code&gt; node in &lt;code&gt;EXPLAIN&lt;/code&gt; wrapping DuckDB's own plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it wins: a CUBE over 10M rows
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;CUBE&lt;/code&gt; across three dimensions computes every combination of &lt;code&gt;channel × country × segment&lt;/code&gt; — 2³ grouping sets in one pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;
             &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rev&lt;/span&gt;
            &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
            &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;CUBE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Native PostgreSQL (&lt;code&gt;SET duckdb.force_execution = false&lt;/code&gt;) takes about 22 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;force_execution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rev&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
           &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;CUBE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                                   &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;--------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;MixedAggregate&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;22501&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;584&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;22501&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;662&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;216&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;Hash&lt;/span&gt; &lt;span class="k"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;
   &lt;span class="n"&gt;Hash&lt;/span&gt; &lt;span class="k"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;
   &lt;span class="n"&gt;Hash&lt;/span&gt; &lt;span class="k"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt;
   &lt;span class="n"&gt;Hash&lt;/span&gt; &lt;span class="k"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;
   &lt;span class="n"&gt;Hash&lt;/span&gt; &lt;span class="k"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;
   &lt;span class="n"&gt;Hash&lt;/span&gt; &lt;span class="k"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt;
   &lt;span class="n"&gt;Hash&lt;/span&gt; &lt;span class="k"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;
   &lt;span class="k"&gt;Group&lt;/span&gt; &lt;span class="k"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
   &lt;span class="n"&gt;Batches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="n"&gt;Memory&lt;/span&gt; &lt;span class="k"&gt;Usage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;264&lt;/span&gt;&lt;span class="n"&gt;kB&lt;/span&gt;
   &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2824&lt;/span&gt; &lt;span class="k"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;87288&lt;/span&gt;
   &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;Seq&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;782&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;1175&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;074&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10000000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2824&lt;/span&gt; &lt;span class="k"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;87288&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;247&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
 &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;22501&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;821&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DuckDB (&lt;code&gt;SET duckdb.force_execution = true&lt;/code&gt;) is two times faster (look at &lt;code&gt;Total Time&lt;/code&gt;, not &lt;code&gt;Execution Time&lt;/code&gt; which doesn't include the time in DuckDB):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;force_execution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;
            &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rev&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
           &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;CUBE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                                                                           &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;----------------------------------------------------------------------------------------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;Custom&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DuckDBScan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;DuckDB&lt;/span&gt; &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="n"&gt;Plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌───────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;    &lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;Profiling&lt;/span&gt; &lt;span class="n"&gt;Information&lt;/span&gt;    &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└───────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rev&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pgduckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;CUBE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="err"&gt;┌────────────────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌──────────────────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;              &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;              &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└──────────────────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└────────────────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌───────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;QUERY&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="n"&gt;EXPLAIN_ANALYZE&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="n"&gt;HASH_GROUP_BY&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="n"&gt;Groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Aggregates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;count_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="mi"&gt;216&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;channel_id&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="n"&gt;country&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="n"&gt;segment&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;amount&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;TABLE_SCAN&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="k"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;87&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└───────────────────────────┘&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;356&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
 &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;257&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;63&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The TABLE_SCAN's &lt;code&gt;20.87s&lt;/code&gt; is aggregated CPU/thread time, not wall-clock time, while &lt;code&gt;Total Time: 12.46s&lt;/code&gt; is elapsed wall time. &lt;code&gt;pg_duckdb&lt;/code&gt; scans the Postgres heap with parallelism at two layers — Postgres parallel workers feed tuples, and multiple DuckDB threads consume and convert them — so the per-operator timing adds up across threads and can exceed the total elapsed time. Additionally, execution is pipelined: the HASH_GROUP_BY consumes scan output concurrently rather than after it finishes.&lt;/p&gt;

&lt;p&gt;Postgres's &lt;code&gt;MixedAggregate&lt;/code&gt; processes all 10M rows one-at-a-time through eight grouping sets on a single backend. DuckDB does a single vectorized &lt;code&gt;HASH_GROUP_BY&lt;/code&gt; over columnar chunks fed by a parallel scan.&lt;/p&gt;

&lt;p&gt;Is the win just the parallel scan, though? It's a fair question — the native CUBE runs single-threaded (grouping sets disable parallel aggregation in PostgreSQL, so there's no &lt;code&gt;Gather&lt;/code&gt; in that plan). I checked by forcing the native scan parallel (&lt;code&gt;max_parallel_workers_per_gather = 4&lt;/code&gt;): a &lt;code&gt;Gather&lt;/code&gt; + &lt;code&gt;Parallel Seq Scan&lt;/code&gt; appears, the scan finishes in ~1.9s instead of ~1.3s — and the query still takes 22.8s, no improvement. The reason is that the scan was never the bottleneck: a bare &lt;code&gt;count(*)&lt;/code&gt; over this table is ~1.5s whether serial or parallel, so the scan is barely a second of the 22s. The other ~21s is the &lt;code&gt;MixedAggregate&lt;/code&gt; maintaining eight grouping sets row by row. That is exactly what DuckDB replaces with one vectorized pass. So the win here is the vectorized aggregation, not parallelism on the scan — parallelizing the scan changes nothing because there was almost nothing to parallelize.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it doesn't win: a plain aggregate
&lt;/h2&gt;

&lt;p&gt;Not every query benefits from offloading to DuckDB. A full-table sum is an all-scan with no additional work on top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;force_execution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                                             &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;-----------------------------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;Finalize&lt;/span&gt; &lt;span class="k"&gt;Aggregate&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;11499&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;774&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;11507&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;049&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4904&lt;/span&gt; &lt;span class="k"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;85208&lt;/span&gt;
   &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;Gather&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;11499&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;441&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;11507&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;032&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="n"&gt;Workers&lt;/span&gt; &lt;span class="n"&gt;Planned&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
         &lt;span class="n"&gt;Workers&lt;/span&gt; &lt;span class="n"&gt;Launched&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
         &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4904&lt;/span&gt; &lt;span class="k"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;85208&lt;/span&gt;
         &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="k"&gt;Partial&lt;/span&gt; &lt;span class="k"&gt;Aggregate&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;11488&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;395&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;11488&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;397&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
               &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4904&lt;/span&gt; &lt;span class="k"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;85208&lt;/span&gt;
               &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;Parallel&lt;/span&gt; &lt;span class="n"&gt;Seq&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;764&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;10322&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;313&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                     &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4904&lt;/span&gt; &lt;span class="k"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;85208&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;086&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
 &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;11507&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;146&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Native PostgreSQL does this in 11 seconds with a parallel scan. DuckDB doesn't improve that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;force_execution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                                                  &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;--------------------------------------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;Custom&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DuckDBScan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;002&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;DuckDB&lt;/span&gt; &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="n"&gt;Plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌───────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;    &lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;Profiling&lt;/span&gt; &lt;span class="n"&gt;Information&lt;/span&gt;    &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└───────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;avg&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pgduckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;
 &lt;span class="err"&gt;┌────────────────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌──────────────────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;              &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;81&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;              &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└──────────────────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└────────────────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌───────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;QUERY&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="n"&gt;EXPLAIN_ANALYZE&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="n"&gt;UNGROUPED_AGGREGATE&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Aggregates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;count_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;08&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;amount&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;amount&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;TABLE_SCAN&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="k"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└───────────────────────────┘&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;093&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
 &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;018&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;58&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query &lt;em&gt;is&lt;/em&gt; the scan, so DuckDB only adds the cost of pulling rows across the boundary and vectorizing them — including decoding that &lt;code&gt;numeric(10,2)&lt;/code&gt; column — while PostgreSQL aggregates its own heap in place with parallel workers. When there's nothing above the scan to vectorize, there's nothing to win.&lt;/p&gt;

&lt;p&gt;So the rule of thumb isn't "turn DuckDB on for analytics". It's "use it where there's work above the scan". Cubes, rollups, grouping sets, and window functions are good candidates. Full-table sums, plain scans, and point lookups probably aren't, since PostgreSQL already handles those well in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use &lt;code&gt;pg_hint_plan&lt;/code&gt; to force at the query scope
&lt;/h2&gt;

&lt;p&gt;Note that &lt;code&gt;duckdb.force_execution&lt;/code&gt; is a session setting, not a per-query hint — once enabled, pg_duckdb routes everything it can through DuckDB. In practice, that means running analytical workloads on a dedicated connection or role with it enabled, rather than toggling it statement by statement. You can also use &lt;code&gt;SET LOCAL&lt;/code&gt; to limit the scope to a transaction, but that requires multiple calls.&lt;/p&gt;

&lt;p&gt;Alternatively, use &lt;code&gt;pg_hint_plan&lt;/code&gt;, which is also available on Azure Database for PostgreSQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;dconfig&lt;/span&gt; &lt;span class="n"&gt;pg_hint_plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable_hint&lt;/span&gt;

 &lt;span class="n"&gt;List&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt; &lt;span class="k"&gt;parameters&lt;/span&gt;
        &lt;span class="k"&gt;Parameter&lt;/span&gt;         &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;
&lt;span class="c1"&gt;--------------------------+-------&lt;/span&gt;
 &lt;span class="n"&gt;pg_hint_plan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable_hint&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;dconfig&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;force_execution&lt;/span&gt;

&lt;span class="n"&gt;List&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt; &lt;span class="k"&gt;parameters&lt;/span&gt;
       &lt;span class="k"&gt;Parameter&lt;/span&gt;        &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;
&lt;span class="c1"&gt;------------------------+-------&lt;/span&gt;
 &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;force_execution&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;off&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;-- the default: do not involve DuckDB for local tables&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;cust_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cust_id&lt;/span&gt; &lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;prod_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                                      &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;--------------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;GroupAggregate&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7863&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;960&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;7863&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;963&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;Group&lt;/span&gt; &lt;span class="k"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cust_id&lt;/span&gt;
   &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;prod_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;Rows&lt;/span&gt; &lt;span class="n"&gt;Removed&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt;
   &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4680&lt;/span&gt; &lt;span class="k"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;85411&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;temp&lt;/span&gt; &lt;span class="k"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;44038&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;44159&lt;/span&gt;
   &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;Sort&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5251&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;976&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;6707&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;319&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10000000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="n"&gt;Sort&lt;/span&gt; &lt;span class="k"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cust_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prod_id&lt;/span&gt;
         &lt;span class="n"&gt;Sort&lt;/span&gt; &lt;span class="k"&gt;Method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;external&lt;/span&gt; &lt;span class="n"&gt;merge&lt;/span&gt;  &lt;span class="n"&gt;Disk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;176152&lt;/span&gt;&lt;span class="n"&gt;kB&lt;/span&gt;
         &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4680&lt;/span&gt; &lt;span class="k"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;85411&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;temp&lt;/span&gt; &lt;span class="k"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;44038&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;44159&lt;/span&gt;
         &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;Seq&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;291&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;1226&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;341&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10000000&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
               &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4680&lt;/span&gt; &lt;span class="k"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;85411&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;089&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
 &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9346&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;363&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;-- add a hint to force for this query only&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="cm"&gt;/*+ SET( duckdb.force_execution on ) */&lt;/span&gt;
           &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;cust_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cust_id&lt;/span&gt; &lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;prod_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                                                    &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;------------------------------------------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;Custom&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DuckDBScan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;002&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;DuckDB&lt;/span&gt; &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="n"&gt;Plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

 &lt;span class="err"&gt;┌─────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌───────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;    &lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;Profiling&lt;/span&gt; &lt;span class="n"&gt;Information&lt;/span&gt;    &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└───────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;cust_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pgduckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cust_id&lt;/span&gt; &lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;prod_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="err"&gt;┌────────────────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌──────────────────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;               &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;08&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;              &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└──────────────────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└────────────────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌───────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;QUERY&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="n"&gt;EXPLAIN_ANALYZE&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;FILTER&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;prod_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="n"&gt;HASH_GROUP_BY&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;Groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Aggregates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;     &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="n"&gt;cust_id&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="n"&gt;prod_id&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;TABLE_SCAN&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="k"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="n"&gt;cust_id&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="n"&gt;prod_id&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└───────────────────────────┘&lt;/span&gt;


 &lt;span class="n"&gt;Planning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;966&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
 &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;973&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;78&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example demonstrates DuckDB finishing the task in 2 seconds, whereas PostgreSQL requires 9 seconds to list customers who ordered more than one product. There is no risk in leaving the parameter as it was set within the query scope with a hint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The precision detail
&lt;/h2&gt;

&lt;p&gt;I declared &lt;code&gt;amount&lt;/code&gt; as &lt;code&gt;numeric(10,2)&lt;/code&gt; on purpose. A bare &lt;code&gt;numeric&lt;/code&gt; (no precision) is something DuckDB can't represent, so &lt;code&gt;pg_duckdb&lt;/code&gt; falls back to PostgreSQL — &lt;code&gt;force_execution = true&lt;/code&gt; and all, your query just runs on the normal executor. The only tell is a &lt;code&gt;WARNING&lt;/code&gt; at plan time (the prepared query fails inside &lt;code&gt;CreatePlan&lt;/code&gt;, Postgres emits a warning, then falls back), which is easy to miss, plus the absence of &lt;code&gt;Custom Scan (DuckDBScan)&lt;/code&gt; in &lt;code&gt;EXPLAIN&lt;/code&gt;. If the top node isn't a &lt;code&gt;DuckDBScan&lt;/code&gt;, DuckDB declined.&lt;/p&gt;

&lt;p&gt;If you want DuckDB to run it anyway, &lt;code&gt;SET duckdb.convert_unsupported_numeric_to_double = true&lt;/code&gt; casts the unsupported &lt;code&gt;numeric&lt;/code&gt; to &lt;code&gt;DOUBLE&lt;/code&gt; instead of falling back — fine for aggregates where a little floating-point imprecision is acceptable, not when you need exact decimal arithmetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Enable PG_DUCKDB on Azure Database for PostgreSQL (Flexible Server) with &lt;code&gt;azure.extensions&lt;/code&gt; + &lt;code&gt;shared_preload_libraries&lt;/code&gt; + restart, then &lt;code&gt;CREATE EXTENSION pg_duckdb&lt;/code&gt;. Add PG_HINT_PLAN if you want to force per query.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SET duckdb.force_execution = true&lt;/code&gt; runs analytics over your existing tables — no Parquet, no data movement, but a faster engine.&lt;/li&gt;
&lt;li&gt;It wins on heavy grouping, not on plain scans. Read the plan: &lt;code&gt;DuckDBScan&lt;/code&gt; means it ran in DuckDB.&lt;/li&gt;
&lt;li&gt;Declare &lt;code&gt;numeric(p,s)&lt;/code&gt; precision on columns you aggregate, or DuckDB falls back (or set &lt;code&gt;convert_unsupported_numeric_to_double&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Control the scope of &lt;code&gt;duckdb.force_execution&lt;/code&gt; at session, transaction, or query level.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a future post, I'll demonstrate how &lt;code&gt;pg_duckdb&lt;/code&gt; can query Parquet files stored in Azure Blob Storage to archive cold data from Postgres and enable heterogeneous queries. We might even use &lt;code&gt;pg_durable&lt;/code&gt; to automate lifecycle management. While it can read Parquet files, the true strength of &lt;code&gt;pg_duckdb&lt;/code&gt; is executing analytics across diverse data sources.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>duckdb</category>
      <category>azure</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Full pg_duckdb on Azure Database for PostgreSQL</title>
      <dc:creator>Franck Pachot</dc:creator>
      <pubDate>Thu, 08 Oct 2026 11:26:47 +0000</pubDate>
      <link>https://dev.to/franckpachot/the-full-pgduckdb-extension-on-azure-database-for-postgresql-2hh7</link>
      <guid>https://dev.to/franckpachot/the-full-pgduckdb-extension-on-azure-database-for-postgresql-2hh7</guid>
      <description>&lt;p&gt;In my previous post, &lt;a href="https://dev.to/franckpachot/amazon-auroras-analytics-is-duckdb-a-reproducible-side-by-side-with-pgduckdb-5343"&gt;Amazon Aurora's analytics is gated DuckDB: comparison with pg_duckdb&lt;/a&gt;, I examined Aurora Analytics in detail alongside &lt;code&gt;pg_duckdb&lt;/code&gt;. I noted that the &lt;code&gt;pg_duckdb&lt;/code&gt; extension, installed on a PostgreSQL container, offers more features. For this article, I configured it on Azure Database for PostgreSQL (Flexible Server), a managed service running the community PostgreSQL with popular open-source extensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling pg_duckdb on Azure Database for PostgreSQL
&lt;/h2&gt;

&lt;p&gt;I've run this demonstration on Azure Database for PostgreSQL flexible server newly provisioned in Canada Central region, with 2 vCores, 4 GiB RAM, 32 GiB storage, and PostgreSQL 18.6:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq6neenn6n8hesms31mz5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq6neenn6n8hesms31mz5.png" alt=" " width="800" height="210"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;pg_duckdb&lt;/code&gt; extension is a &lt;code&gt;shared_preload_libraries&lt;/code&gt; extension, so two settings are needed. First, allow-list it, then add it to the preload libraries and restart.&lt;/p&gt;

&lt;p&gt;The cover image of this article shows the manual way in the portal's Server parameters, with two clicks on the checkboxes, or you can do the same via the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# allow-list the extension&lt;/span&gt;
az postgres flexible-server parameter &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &amp;lt;rg&amp;gt; &lt;span class="nt"&gt;--server-name&lt;/span&gt; &amp;lt;server&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; azure.extensions &lt;span class="nt"&gt;--value&lt;/span&gt; pg_duckdb

&lt;span class="c"&gt;# load the library (append to whatever is already there), then restart&lt;/span&gt;
az postgres flexible-server parameter &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &amp;lt;rg&amp;gt; &lt;span class="nt"&gt;--server-name&lt;/span&gt; &amp;lt;server&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; shared_preload_libraries &lt;span class="nt"&gt;--value&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;existing&amp;gt;,pg_duckdb"&lt;/span&gt;

az postgres flexible-server restart &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &amp;lt;rg&amp;gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &amp;lt;server&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you only do the first step, &lt;code&gt;CREATE EXTENSION&lt;/code&gt; fails with a clear hint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: pg_duckdb needs to be loaded via shared_preload_libraries
HINT:  Add pg_duckdb to shared_preload_libraries.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the restart, with a normal login, you CREATE EXTENSION in the database where you want to run DuckDB functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;pg_duckdb&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
                         &lt;span class="n"&gt;List&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;installed&lt;/span&gt; &lt;span class="n"&gt;extensions&lt;/span&gt;
   &lt;span class="n"&gt;Name&lt;/span&gt;    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Version&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Default&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;Schema&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;         &lt;span class="n"&gt;Description&lt;/span&gt;
&lt;span class="c1"&gt;-----------+---------+-----------------+--------+-----------------------------&lt;/span&gt;
 &lt;span class="n"&gt;pg_duckdb&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;   &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;           &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;DuckDB&lt;/span&gt; &lt;span class="n"&gt;Embedded&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Postgres&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;extname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extversion&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_extension&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;extname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pg_duckdb'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;extname&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;extversion&lt;/span&gt;
&lt;span class="c1"&gt;-----------+------------&lt;/span&gt;
 &lt;span class="n"&gt;pg_duckdb&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SELECT version();'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

 &lt;span class="nv"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;-------------&lt;/span&gt;
 &lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's how you load an extension in PostgreSQL — since this is PostgreSQL itself, not a re-implementation. You load &lt;code&gt;pg_duckdb&lt;/code&gt; using the same &lt;code&gt;shared_preload_libraries&lt;/code&gt; and &lt;code&gt;CREATE EXTENSION&lt;/code&gt; commands you'd use anywhere else, and everything afterward works exactly like the standard community server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading Parquet from Azure Blob
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;pg_duckdb&lt;/code&gt; extension reads object storage through a DuckDB secret. For Blob, a connection string is the simplest form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_azure_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'DefaultEndpointsProtocol=https;AccountName=…;AccountKey=…;EndpointSuffix=core.windows.net'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;create_azure_secret&lt;/span&gt;
&lt;span class="c1"&gt;---------------------&lt;/span&gt;
 &lt;span class="n"&gt;azure_secret_42&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I uploaded the same &lt;code&gt;title.parquet&lt;/code&gt; used in the previous post (500,000 rows: &lt;code&gt;id, title, kind_id, production_year&lt;/code&gt;) to a container named &lt;code&gt;lake&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzcxfdu4aplbdkcl2zvi2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzcxfdu4aplbdkcl2zvi2.png" alt=" " width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then a Blob path is just a table source for DuckDB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/job/title.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="k"&gt;count&lt;/span&gt;
&lt;span class="c1"&gt;--------&lt;/span&gt;
 &lt;span class="mi"&gt;500000&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'production_year'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/job/title.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
           &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="n"&gt;title&lt;/span&gt;  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;production_year&lt;/span&gt;
&lt;span class="c1"&gt;----+---------+-----------------&lt;/span&gt;
  &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;            &lt;span class="mi"&gt;1921&lt;/span&gt;
  &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;            &lt;span class="mi"&gt;1922&lt;/span&gt;
  &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;            &lt;span class="mi"&gt;1923&lt;/span&gt;
  &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;            &lt;span class="mi"&gt;1924&lt;/span&gt;
  &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;            &lt;span class="mi"&gt;1925&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The execution plan is exposed by &lt;code&gt;pg_duckdb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'production_year'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/job/title.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
           &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                                                              &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;---------------------------------------------------------------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;Custom&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DuckDBScan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;002&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;production_year&lt;/span&gt;
   &lt;span class="n"&gt;DuckDB&lt;/span&gt; &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="n"&gt;Plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌───────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;    &lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;Profiling&lt;/span&gt; &lt;span class="n"&gt;Information&lt;/span&gt;    &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└───────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;production_year&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/job/title.parquet'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
 &lt;span class="err"&gt;┌────────────────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌──────────────────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;              &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;374&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;              &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└──────────────────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└────────────────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌───────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;QUERY&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="n"&gt;EXPLAIN_ANALYZE&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="n"&gt;STREAMING_LIMIT&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;TABLE_SCAN&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="k"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;READ_PARQUET&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;title&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="n"&gt;production_year&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;096&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└───────────────────────────┘&lt;/span&gt;
 &lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;Identifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7627283629006825662&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;254&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;221&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
 &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;249&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;641&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;51&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the DuckDB representation of &lt;a href="https://duckdb.org/docs/lts/guides/meta/explain_analyze" rel="noopener noreferrer"&gt;EXPLAIN ANALYZE&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same query, the same plan
&lt;/h2&gt;

&lt;p&gt;Here is the query from the last post, run against the Blob file through &lt;code&gt;pg_duckdb&lt;/code&gt; on Azure Database for PostgreSQL (Flexible Server):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'production_year'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;production_year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/job/title.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
           &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'kind_id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
           &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'production_year'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
           &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                                                                                                           &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;Custom&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DuckDBScan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;002&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;003&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;production_year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;
   &lt;span class="n"&gt;DuckDB&lt;/span&gt; &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="n"&gt;Plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌───────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;    &lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;Profiling&lt;/span&gt; &lt;span class="n"&gt;Information&lt;/span&gt;    &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└───────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;production_year&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;production_year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/job/title.parquet'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;production_year&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
 &lt;span class="err"&gt;┌────────────────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌──────────────────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;               &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;              &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└──────────────────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└────────────────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌───────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;QUERY&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="n"&gt;EXPLAIN_ANALYZE&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="n"&gt;__internal_decompress_integ&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="n"&gt;ral_integer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="n"&gt;ORDER_BY&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;     &lt;span class="n"&gt;count_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;     &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="n"&gt;__internal_compress_integra&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="n"&gt;l_utinyint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="n"&gt;__internal_decompress_integ&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="n"&gt;ral_integer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;             &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;   &lt;span class="n"&gt;PERFECT_HASH_GROUP_BY&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;Groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Aggregates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;count_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="n"&gt;production_year&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;&lt;span class="n"&gt;__internal_compress_integra&lt;/span&gt;&lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="n"&gt;l_utinyint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;TABLE_SCAN&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="k"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;READ_PARQUET&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="n"&gt;production_year&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;     &lt;span class="n"&gt;Filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kind_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="n"&gt;Files&lt;/span&gt; &lt;span class="k"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└───────────────────────────┘&lt;/span&gt;
 &lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;Identifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4315756270624112827&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;678&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;580&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
 &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;252&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;476&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;112&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's how the DuckDB extension displays its execution plan within the PostgreSQL query plan: &lt;code&gt;pg_duckdb&lt;/code&gt; draws its box tree. DuckDB is a different engine, gathering execution statistics differently and &lt;code&gt;pg_duckdb&lt;/code&gt; displays it as-is so you can understand them better. For example, each box shows that operator's thread-summed CPU time, per-node and not cumulative — unlike PostgreSQL's EXPLAIN ANALYZE, where a parent node's time includes its children.&lt;/p&gt;

&lt;p&gt;With a different display, this is the identical plan operations produced though &lt;code&gt;aurora_analytics&lt;/code&gt; for the same file in the previous post — down to &lt;code&gt;__internal_compress_integral_utinyint(#0, 1920)&lt;/code&gt;, DuckDB's frame-of-reference integer compression with the base constant &lt;code&gt;1920&lt;/code&gt; (the minimum &lt;code&gt;production_year&lt;/code&gt; in the data).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Custom Scan (DuckDBScan)
  DuckDB Execution Plan:
  PROJECTION   __internal_decompress_integral_integer(#0, 1920), #1
  ORDER_BY     count_star() DESC
  PROJECTION   __internal_compress_integral_utinyint(#0, 1920), #1
  PERFECT_HASH_GROUP_BY   Groups: #0   Aggregates: count_star()
  PROJECTION   production_year
  PROJECTION   __internal_compress_integral_utinyint(#0, 1920)
  READ_PARQUET
     Function: READ_PARQUET
     Projections: production_year
     Filters: kind_id=1
     ~100,000 rows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It fully matches the open-source &lt;code&gt;pg_duckdb&lt;/code&gt; running in a local Docker container. Three independent deployments — a laptop, Aurora, and Azure Database for PostgreSQL — emit the same DuckDB plan for the same query. It is the same engine in all three. But there's more in the &lt;code&gt;pg_duckdb&lt;/code&gt; extension.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the extension approach actually differs
&lt;/h2&gt;

&lt;p&gt;Same engine, so the interesting part is the packaging. Here Azure's choice — ship the extension itself — leads to concrete, observable differences from Aurora's closed FDW.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The engine is directly usable
&lt;/h3&gt;

&lt;p&gt;On Aurora, DuckDB is sealed behind the FDW: &lt;code&gt;read_parquet()&lt;/code&gt; as a function, list/struct literals, &lt;code&gt;QUALIFY&lt;/code&gt;, &lt;code&gt;duckdb.query()&lt;/code&gt; — all are rejected, because only a foreign-table scan is handed to DuckDB. On Azure Database for PostgreSQL, the whole extension surface is present. &lt;code&gt;\dx+ pg_duckdb&lt;/code&gt; lists hundreds of objects (235 functions, 23 types, an FDW, casts and operators), and you can call DuckDB directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; 
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;'SELECT 1 AS a, [1,2,3] AS lst
'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
 &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;   &lt;span class="n"&gt;lst&lt;/span&gt;
&lt;span class="c1"&gt;---+---------&lt;/span&gt;
 &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a DuckDB list literal evaluated by DuckDB, returned to PostgreSQL as an &lt;code&gt;integer[]&lt;/code&gt; array datatype. On Aurora, it's a parser error. If you want DuckDB's SQL dialect — not just its scan — the extension gives it to you.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. It accelerates your existing PostgreSQL tables
&lt;/h3&gt;

&lt;p&gt;This is the sharpest functional difference. Aurora's &lt;code&gt;aurora_analytics&lt;/code&gt; only engages DuckDB for foreign (S3) tables. A query on a regular Aurora table runs on the normal PostgreSQL executor. With &lt;code&gt;pg_duckdb&lt;/code&gt; you can push a query on an ordinary heap table through DuckDB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;local_sales&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
           &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;amt&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;200000&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;force_execution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;cust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;local_sales&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cust&lt;/span&gt;
           &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
                                                                    &lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;--------------------------------------------------------------------------------------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="n"&gt;Custom&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DuckDBScan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;001&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;
   &lt;span class="n"&gt;DuckDB&lt;/span&gt; &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="n"&gt;Plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌───────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;    &lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;Profiling&lt;/span&gt; &lt;span class="n"&gt;Information&lt;/span&gt;    &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└───────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;cust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pgduckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local_sales&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cust&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
 &lt;span class="err"&gt;┌────────────────────────────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│┌──────────────────────────────────────────────┐│&lt;/span&gt;
 &lt;span class="err"&gt;││&lt;/span&gt;              &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;126&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;              &lt;span class="err"&gt;││&lt;/span&gt;
 &lt;span class="err"&gt;│└──────────────────────────────────────────────┘│&lt;/span&gt;
 &lt;span class="err"&gt;└────────────────────────────────────────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌───────────────────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;QUERY&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;      &lt;span class="n"&gt;EXPLAIN_ANALYZE&lt;/span&gt;      &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;TOP_N&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="n"&gt;Top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="k"&gt;Order&lt;/span&gt; &lt;span class="k"&gt;By&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;     &lt;span class="n"&gt;count_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;     &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;           &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;01&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="n"&gt;HASH_GROUP_BY&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;Groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Aggregates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;count_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;PROJECTION&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;            &lt;span class="n"&gt;cust&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;            &lt;span class="n"&gt;amt&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└─────────────┬─────────────┘&lt;/span&gt;
 &lt;span class="err"&gt;┌─────────────┴─────────────┐&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;         &lt;span class="n"&gt;TABLE_SCAN&lt;/span&gt;        &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;    &lt;span class="err"&gt;────────────────────&lt;/span&gt;   &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;     &lt;span class="k"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;local_sales&lt;/span&gt;    &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;            &lt;span class="n"&gt;cust&lt;/span&gt;           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;            &lt;span class="n"&gt;amt&lt;/span&gt;            &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;                           &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;       &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;│&lt;/span&gt;          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="err"&gt;│&lt;/span&gt;
 &lt;span class="err"&gt;└───────────────────────────┘&lt;/span&gt;
 &lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;Identifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6568044750341216650&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;394&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
 &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;360&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;75&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs DuckDB scans and aggregation on PostgreSQL tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Custom Scan (DuckDBScan)
  TOP_N  Top: 5  Order By: count_star() DESC
  HASH_GROUP_BY  Aggregates: count_star(), sum(#1)
  PROJECTION  cust, amt
  PGDUCKDB_POSTGRES_SCAN  Table: local_sales
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DuckDB is now the executor, reading the PostgreSQL heap through &lt;code&gt;PGDUCKDB_POSTGRES_SCAN&lt;/code&gt;. Whether that is faster than the native plan depends entirely on the query — it is a win for scans, joins and big aggregations, and a loss for point lookups — but the point here is that the option exists at all, which it does not on Aurora's proprietary extension.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. It is the same extension you run anywhere
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pg_duckdb&lt;/code&gt; on Azure Database for PostgreSQL is the same open-source extension (version 1.1.0 here) you can run in Docker on a laptop or on a self-managed server. The plans I showed match my local container exactly. There is no Azure-specific dialect to learn: it is the community extension, managed. That's the freedom PostgreSQL users expect: they run on a managed service because the operations, reliability, and performance provide the best experience, not because they are locked into something different from the open source postgres with extensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  When pg_duckdb on Azure is different than self-managed?
&lt;/h2&gt;

&lt;p&gt;Being honest, any managed service that packages an extension might also need to impose constraints. The hardening is real, and one capability—write-back to the lake—requires a detour you should be aware of.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hardening from superuser configuration
&lt;/h3&gt;

&lt;p&gt;Azure Database for PostgreSQL locks &lt;code&gt;pg_duckdb&lt;/code&gt; access to local directories. These are all &lt;code&gt;superuser&lt;/code&gt;-context settings, and a normal login can't touch them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;dconfig&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
                             &lt;span class="n"&gt;List&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt; &lt;span class="k"&gt;parameters&lt;/span&gt;
                   &lt;span class="k"&gt;Parameter&lt;/span&gt;                    &lt;span class="o"&gt;|&lt;/span&gt;                  &lt;span class="n"&gt;Value&lt;/span&gt;
&lt;span class="c1"&gt;------------------------------------------------+-----------&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
 &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allowed_directories&lt;/span&gt;                     &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
 &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable_external_access&lt;/span&gt;                  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;off&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;27&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The effect: only &lt;code&gt;az://&lt;/code&gt; is reachable. Local files are blocked outright:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
           &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/tmp/my.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PGDuckDB&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;CreatePlan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Prepared&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="n"&gt;returned&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Permission&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt; &lt;span class="n"&gt;LocalFileSystem&lt;/span&gt; &lt;span class="n"&gt;has&lt;/span&gt; &lt;span class="n"&gt;been&lt;/span&gt; &lt;span class="n"&gt;disabled&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also blocks &lt;code&gt;s3://&lt;/code&gt; and arbitrary &lt;code&gt;https://&lt;/code&gt; URLs. That is a deliberate, security-first posture (DuckDB can otherwise fetch arbitrary URLs), and it is the right default for a managed service, but it does mean the extension is not exactly the "wide open DuckDB" you get self-managed.&lt;/p&gt;

&lt;p&gt;These are the extension's own settings, not an Azure bolt-on. &lt;code&gt;duckdb.allowed_directories&lt;/code&gt; and &lt;code&gt;duckdb.enable_external_access&lt;/code&gt; are upstream &lt;code&gt;pg_duckdb&lt;/code&gt; GUCs (&lt;a href="https://github.com/duckdb/pg_duckdb/commit/d5b95c4b76fedd80de3656e34794abbb1a869a96" rel="noopener noreferrer"&gt;added March 2026&lt;/a&gt;) — &lt;code&gt;allowed_directories&lt;/code&gt; allowlists prefixes that stay reachable even when &lt;code&gt;enable_external_access&lt;/code&gt; is off. So Azure is configuring the community extension's built-in allow-list, not patching restrictions on top of it. &lt;code&gt;enable_external_access&lt;/code&gt; is also applied at initialization and can't be flipped at runtime afterward — DuckDB rejects changes to &lt;code&gt;allowed_directories&lt;/code&gt; once external access is disabled — which is why it reads as a fixed, image-level setting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write-back works, but not with the &lt;code&gt;COPY TO&lt;/code&gt; syntax
&lt;/h3&gt;

&lt;p&gt;In the self-managed world, you can write results back to object storage directly with a PostgreSQL command: &lt;code&gt;COPY (…) TO 's3://…'&lt;/code&gt; or &lt;code&gt;az://&lt;/code&gt;. As a normal &lt;code&gt;azure_pg_admin&lt;/code&gt; login, though, that statement is refused:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;cust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
            &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;local_sales&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cust&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'az://lake/exports/agg.parquet'&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;relative&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Azure&lt;/span&gt; &lt;span class="k"&gt;Database&lt;/span&gt; &lt;span class="k"&gt;For&lt;/span&gt; &lt;span class="n"&gt;PostgreSQL&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In PostgreSQL, a server-side &lt;code&gt;COPY … TO &amp;lt;file&amp;gt;&lt;/code&gt; requires superuser privileges or the &lt;code&gt;pg_write_server_files&lt;/code&gt; role, neither of which Azure Database for PostgreSQL grants to a normal login. It also rejects non-absolute paths before the privilege check, so the error you see depends on the path shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- any path that doesn't start with "/" is classed as relative, rejected first:&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'az://lake/my.parquet'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;relative&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Azure&lt;/span&gt; &lt;span class="k"&gt;Database&lt;/span&gt; &lt;span class="k"&gt;For&lt;/span&gt; &lt;span class="n"&gt;PostgreSQL&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'relative/my.csv'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;relative&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Azure&lt;/span&gt; &lt;span class="k"&gt;Database&lt;/span&gt; &lt;span class="k"&gt;For&lt;/span&gt; &lt;span class="n"&gt;PostgreSQL&lt;/span&gt;

&lt;span class="c1"&gt;-- a genuine absolute path gets past that and hits the privilege check:&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'/tmp/my.csv'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;must&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="n"&gt;superuser&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;
&lt;span class="n"&gt;HINT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;Anyone&lt;/span&gt; &lt;span class="n"&gt;can&lt;/span&gt; &lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="k"&gt;stdout&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="k"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;psql&lt;/span&gt;&lt;span class="s1"&gt;'s &lt;/span&gt;&lt;span class="se"&gt;\c&lt;/span&gt;&lt;span class="s1"&gt;opy command also works for anyone.

postgres=&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error message names Azure for a reason: the managed service adds its own security gate ahead of PostgreSQL's normal COPY checks, and that gate, like PostreSQL, rejects any path without a leading &lt;code&gt;/&lt;/code&gt; as "relative" — &lt;code&gt;az://&lt;/code&gt; included. So an &lt;code&gt;az://&lt;/code&gt; URL is stopped at that first gate before it ever reaches the privilege check, while a genuine absolute path like &lt;code&gt;/tmp/my.csv&lt;/code&gt; clears the gate and then hits PostgreSQL's own superuser requirement. Two separate checks, and a normal login is stopped by one or the other.&lt;/p&gt;

&lt;p&gt;But write-back is &lt;em&gt;not&lt;/em&gt; actually unavailable on Azure Database for PostgreSQL — because the extension exposes the DuckDB engine directly, with its own security that allows access to &lt;code&gt;az://&lt;/code&gt;. You can hand the &lt;code&gt;COPY&lt;/code&gt; straight to DuckDB with &lt;code&gt;duckdb.raw_query&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$$&lt;/span&gt;
             &lt;span class="k"&gt;COPY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
               &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;cust&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
                &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pgduckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local_sales&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;cust&lt;/span&gt;
             &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'az://lake/exports/agg.parquet'&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;parquet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;      
           &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'az://lake/exports/agg.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="k"&gt;count&lt;/span&gt;
&lt;span class="c1"&gt;-------&lt;/span&gt;
  &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This doesn't bypass any security — it writes through DuckDB's own storage controls instead of PostgreSQL's server-file gate, which is unaware of additional extension's capabilities and object storage URLs. The two protect different things: PostgreSQL's gate restricts writes to local filesystem, fir superuser, while DuckDB governs object-storage access through its secret and allow-list (&lt;code&gt;duckdb.allowed_directories&lt;/code&gt;, which here permits only &lt;code&gt;az://&lt;/code&gt;). An object-storage write must go through the layer that actually owns that resource.&lt;/p&gt;

&lt;p&gt;The file is really written to Blob and reads straight back. Three caveats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Qualify Postgres tables as &lt;code&gt;pgduckdb.public.&amp;lt;table&amp;gt;&lt;/code&gt;, because &lt;code&gt;raw_query&lt;/code&gt; runs inside the DuckDB engine, which sees DuckDB's catalog, not PostgreSQL's search path. A bare &lt;code&gt;FROM local_sales&lt;/code&gt; fails with &lt;code&gt;Catalog Error: Table with name local_sales does not exist&lt;/code&gt;. &lt;code&gt;pg_duckdb&lt;/code&gt; mounts your Postgres tables under &lt;code&gt;pgduckdb.public.*&lt;/code&gt;, so that's how you name them here.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;duckdb.query()&lt;/code&gt; won't do it — it only accepts a single &lt;code&gt;SELECT&lt;/code&gt;, so the &lt;code&gt;COPY&lt;/code&gt; must go through &lt;code&gt;raw_query&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The storage allow-list still applies: you can only write under &lt;code&gt;az://&lt;/code&gt; (a local &lt;code&gt;/tmp&lt;/code&gt; target fails with &lt;code&gt;LocalFileSystem has been disabled by configuration&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So write-back is there and under control. It just isn't the PostgreSQL &lt;code&gt;COPY TO&lt;/code&gt;, and you reach your tables through the DuckDB catalog name.&lt;/p&gt;

&lt;p&gt;This is where the two cloud systems really diverge. Aurora's foreign tables are strictly read-only, and I verified every write attempt — not just one. Both &lt;code&gt;INSERT … VALUES&lt;/code&gt;, bulk &lt;code&gt;INSERT … SELECT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt; commands all fail with the same message: &lt;em&gt;"Aurora Analytics foreign tables are read-only — update the source files in S3 … directly"&lt;/em&gt;. Pointing a foreign table to a new S3 path and trying to insert also fails, suggesting the wrapper is the issue, not a locked file. Additionally, &lt;code&gt;COPY (…) TO 's3://…'&lt;/code&gt; results in &lt;em&gt;"COPY to a file is not supported"&lt;/em&gt;, and the extension does not provide any write, export, or unload functions. The FDW has no write capabilities—the DuckDB engine is sealed behind it. &lt;/p&gt;

&lt;p&gt;Conversely, on Azure, &lt;code&gt;pg_duckdb&lt;/code&gt; exposes the DuckDB engine itself. Queries can read from and write to the data lake. The key difference is that the engine is accessible rather than sealed behind a restricted interface. pg_duckdb embeds a DuckDB execution engine alongside PostgreSQL, allowing DuckDB operations on both PostgreSQL tables and external data sources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;pg_duckdb&lt;/code&gt; on Microsoft Azure for PostgreSQL&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;aurora_analytics&lt;/code&gt; on Amazon Aurora (AWS)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it is&lt;/td&gt;
&lt;td&gt;the open-source &lt;code&gt;pg_duckdb&lt;/code&gt; extension&lt;/td&gt;
&lt;td&gt;a closed extension embedding DuckDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engine&lt;/td&gt;
&lt;td&gt;✅ DuckDB (identical plan)&lt;/td&gt;
&lt;td&gt;✅ DuckDB (identical plan)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How you reach the lake&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;read_parquet('az://…')&lt;/code&gt;, &lt;code&gt;r['col']&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CREATE FOREIGN TABLE … OPTIONS(format 'parquet')&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct DuckDB SQL (&lt;code&gt;duckdb.query&lt;/code&gt;, list/struct)&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No — sealed behind the FDW&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accelerate your own PostgreSQL tables&lt;/td&gt;
&lt;td&gt;✅ Yes (&lt;code&gt;force_execution&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;❌  No — foreign tables only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read object storage&lt;/td&gt;
&lt;td&gt;✅ Yes (&lt;code&gt;az://&lt;/code&gt;, secret)&lt;/td&gt;
&lt;td&gt;✅ Yes (&lt;code&gt;s3://&lt;/code&gt;, IAM role)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write back to the lake&lt;/td&gt;
&lt;td&gt;✅ Yes, via &lt;code&gt;duckdb.raw_query&lt;/code&gt; to use the extension's security model&lt;/td&gt;
&lt;td&gt;❌ No — foreign tables read-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External access&lt;/td&gt;
&lt;td&gt;locked to &lt;code&gt;az://&lt;/code&gt;, superuser-only GUCs&lt;/td&gt;
&lt;td&gt;IAM-scoped to S3/Glue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Portability of what you learn&lt;/td&gt;
&lt;td&gt;same extension as laptop/self-managed&lt;/td&gt;
&lt;td&gt;Aurora-specific&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both clouds embed the same DuckDB engine in PostgreSQL. Aurora wraps it as a managed, catalog-oriented, read-only foreign-data wrapper. Azure ships the community extension, so you can use DuckDB directly, accelerate your existing tables, and even write back to the lake. Azure limits only the granted permissions, without changing the open-source software's behavior. Where the managed guardrails bite, they bite on both (external access is locked down on each), but the extension approach keeps the engine in your hands rather than sealed behind an FDW.&lt;/p&gt;

&lt;p&gt;In the next posts of this series, we will go through some examples of using &lt;code&gt;pg_duckdb&lt;/code&gt; in Azure Database for PostgreSQL.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>duckdb</category>
      <category>azure</category>
      <category>datalake</category>
    </item>
    <item>
      <title>Amazon Aurora's analytics is gated DuckDB: comparison with open-source pg_duckdb</title>
      <dc:creator>Franck Pachot</dc:creator>
      <pubDate>Mon, 05 Oct 2026 21:09:00 +0000</pubDate>
      <link>https://dev.to/franckpachot/amazon-auroras-analytics-is-duckdb-a-reproducible-side-by-side-with-pgduckdb-5343</link>
      <guid>https://dev.to/franckpachot/amazon-auroras-analytics-is-duckdb-a-reproducible-side-by-side-with-pgduckdb-5343</guid>
      <description>&lt;p&gt;Microsoft announced the public preview of the &lt;code&gt;pg_duckdb&lt;/code&gt; extension for Azure Database for PostgreSQL Flexible Server at Ignite on November 18, 2025. In August 2026, Amazon announced an agreement to acquire DuckLabs, the company behind DuckDB, but the DuckDB open-source project itself remains independent. On September 30, AWS announced that Amazon Aurora PostgreSQL can use embedded DuckDB to query Apache Iceberg and Parquet data in data lakes alongside operational data. Let's compare.&lt;/p&gt;

&lt;p&gt;Amazon Aurora PostgreSQL can now query Parquet and Iceberg in S3 through the &lt;code&gt;aurora_analytics&lt;/code&gt; extension, and AWS says DuckDB is embedded. I wanted to &lt;em&gt;prove&lt;/em&gt; that rather than take it on faith. The method is simple: get an execution plan out of Aurora, then get one out of a known-DuckDB engine (the open-source &lt;code&gt;pg_duckdb&lt;/code&gt;) for the same query over the same file, and compare. Here is every step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provision Aurora with aurora_analytics
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;aurora_analytics&lt;/code&gt; needs two things: an Aurora PostgreSQL cluster on a supported version (17.11+ or 18.6+), and an IAM role attached to the cluster with the &lt;code&gt;AuroraAnalytics&lt;/code&gt; feature so Aurora can read your S3 bucket. I used a CloudFormation stack The essential pieces are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an Aurora PostgreSQL Serverless v2 cluster, &lt;code&gt;EngineVersion=17.11&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;a cluster parameter group with &lt;code&gt;aurora_analytics.enabled = 1&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;an IAM role trusting &lt;code&gt;rds.amazonaws.com&lt;/code&gt;, with S3 read on the data bucket (and Glue read if you want Iceberg), attached to the cluster via &lt;code&gt;AssociatedRoles&lt;/code&gt; with &lt;code&gt;FeatureName: AuroraAnalytics&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;an S3 bucket for the Parquet data, plus an S3 gateway VPC endpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I deployed it with CloudFormation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation deploy &lt;span class="nt"&gt;--stack-name&lt;/span&gt; aurora-analytics ...

aws cloudformation describe-stacks &lt;span class="nt"&gt;--stack-name&lt;/span&gt; aurora-analytics &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Stacks[0].Outputs'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; table

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I uploaded the Parquet file and connected to Aurora:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
aws s3 &lt;span class="nb"&gt;cp &lt;/span&gt;title.parquet s3://my-bucket/job/title.parquet

psql &lt;span class="s2"&gt;"host=&amp;lt;cluster-endpoint&amp;gt; port=5432 dbname=analytics user=dbadmin sslmode=require"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Query Parquet in Aurora and capture the plan
&lt;/h2&gt;

&lt;p&gt;I enabled the extension and defined a foreign table over the file. The empty column list makes Aurora infer the schema from the Parquet footer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;aurora_analytics&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;title_ft&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;SERVER&lt;/span&gt; &lt;span class="n"&gt;aurora_analytics_server&lt;/span&gt;
  &lt;span class="k"&gt;OPTIONS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="s1"&gt;'s3://my-bucket/job/title.parquet'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt; &lt;span class="s1"&gt;'parquet'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;title_ft&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is the query I compared, and its plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;production_year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;title_ft&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;kind_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;production_year&lt;/span&gt;
 &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt;
&lt;span class="c1"&gt;----------------------------------------------------------------------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="n"&gt;Custom&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;848&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;919&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;aurora_analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;production_year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aurora_analytics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;count&lt;/span&gt;
  &lt;span class="n"&gt;Pushdown&lt;/span&gt; &lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;production_year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;kind_id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;kind_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;production_year&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;production_year&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;aurora_analytics_parameter_1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;title_ft&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;title_ft&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;production_year&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="n"&gt;CPU&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
  &lt;span class="n"&gt;Effective&lt;/span&gt; &lt;span class="n"&gt;Parallelism&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;
  &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="n"&gt;Thread&lt;/span&gt; &lt;span class="n"&gt;Wait&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;PROJECTION&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;637&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;637&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;__internal_decompress_integral_integer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;ORDER_BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;614&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;51&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;614&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="k"&gt;Order&lt;/span&gt; &lt;span class="k"&gt;By&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;count_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
              &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;PROJECTION&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;880&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;880&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;__internal_compress_integral_utinyint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
                    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;PROJECTION&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;873&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;873&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                          &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;__internal_decompress_integral_integer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
                          &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;PERFECT_HASH_GROUP_BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;844&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;844&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                &lt;span class="n"&gt;Groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
                                &lt;span class="n"&gt;Aggregates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;count_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;PROJECTION&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;582&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;982&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                      &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;production_year&lt;/span&gt;
                                      &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;PROJECTION&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;571&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;982&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                            &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;__internal_compress_integral_utinyint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;READ_PARQUET&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;609&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;981&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                                                  &lt;span class="k"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title_ft&lt;/span&gt;
                                                  &lt;span class="n"&gt;Filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kind_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
                                                  &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;production_year&lt;/span&gt;
                                                  &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="n"&gt;Files&lt;/span&gt; &lt;span class="k"&gt;Read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                                                  &lt;span class="k"&gt;Rows&lt;/span&gt; &lt;span class="n"&gt;Removed&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400000&lt;/span&gt;
&lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="n"&gt;Identifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5750582199016602255&lt;/span&gt;
&lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;369&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;237&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;81&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;265&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="n"&gt;Analytics&lt;/span&gt; &lt;span class="k"&gt;Cache&lt;/span&gt; &lt;span class="n"&gt;Hit&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;93&lt;/span&gt;&lt;span class="n"&gt;kB&lt;/span&gt;
&lt;span class="n"&gt;Analytics&lt;/span&gt; &lt;span class="n"&gt;Remote&lt;/span&gt; &lt;span class="k"&gt;Read&lt;/span&gt; &lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;kB&lt;/span&gt;
&lt;span class="n"&gt;S3&lt;/span&gt; &lt;span class="n"&gt;HEAD&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt; &lt;span class="k"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;S3&lt;/span&gt; &lt;span class="k"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt; &lt;span class="k"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is already suggestive: &lt;code&gt;READ_PARQUET&lt;/code&gt;, &lt;code&gt;PERFECT_HASH_GROUP_BY&lt;/code&gt;, &lt;code&gt;count_star()&lt;/code&gt;, and a &lt;code&gt;Pushdown SQL&lt;/code&gt; line that reads &lt;code&gt;FROM system.main.read_parquet(...)&lt;/code&gt;. Those are DuckDB's names. But let's prove it.&lt;/p&gt;

&lt;p&gt;The cover image of this post shows the same with the &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-ossdata.vscode-pgsql" rel="noopener noreferrer"&gt;Microsoft PostgreSQL extension for VSCode&lt;/a&gt; on Kiro.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same query in pg_duckdb on Docker
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pg_duckdb&lt;/code&gt; is the open-source DuckDB-in-PostgreSQL extension. I run it locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; pgduckdb &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;duckdb &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;lab &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 55432:5432 pgduckdb/pgduckdb:17-v1.1.1

&lt;span class="c"&gt;# put the SAME file where the container can read it&lt;/span&gt;
docker &lt;span class="nb"&gt;cp &lt;/span&gt;title.parquet pgduckdb:/tmp/title.parquet

docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; pgduckdb psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-d&lt;/span&gt; lab

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I execute the same query with the &lt;code&gt;pg_duckdb&lt;/code&gt; functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;pg_duckdb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;force_execution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;EXPLAIN&lt;/span&gt;
 &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'production_year'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;production_year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;
 &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/tmp/title.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'kind_id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
 &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'production_year'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pg_duckdb&lt;/code&gt; draws the plan as a box tree:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flhoa8jnjm6g3dvjy18ne.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flhoa8jnjm6g3dvjy18ne.png" alt=" " width="800" height="943"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One rendering difference worth noting: &lt;code&gt;pg_duckdb&lt;/code&gt; prints DuckDB's native plan verbatim, so per-node times are DuckDB's thread-summed CPU (a child can read higher than the total wall time). That's different from Aurora which reformats the DuckDB plan into PostgreSQL's convention, where each node's time is cumulative wall-clock and the top node matches Execution Time. &lt;/p&gt;

&lt;p&gt;Here are the operators:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Custom&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DuckDBScan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;DuckDB&lt;/span&gt; &lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="n"&gt;Plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

  &lt;span class="n"&gt;PROJECTION&lt;/span&gt;   &lt;span class="n"&gt;__internal_decompress_integral_integer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="n"&gt;ORDER_BY&lt;/span&gt;     &lt;span class="n"&gt;count_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="n"&gt;PROJECTION&lt;/span&gt;   &lt;span class="n"&gt;__internal_compress_integral_utinyint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="n"&gt;PERFECT_HASH_GROUP_BY&lt;/span&gt;   &lt;span class="n"&gt;Groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;   &lt;span class="n"&gt;Aggregates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;count_star&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;PROJECTION&lt;/span&gt;   &lt;span class="n"&gt;production_year&lt;/span&gt;
  &lt;span class="n"&gt;PROJECTION&lt;/span&gt;   &lt;span class="n"&gt;__internal_compress_integral_utinyint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;READ_PARQUET&lt;/span&gt;
     &lt;span class="k"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;READ_PARQUET&lt;/span&gt;
     &lt;span class="n"&gt;Projections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;production_year&lt;/span&gt;
     &lt;span class="n"&gt;Filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;kind_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
     &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We recognize the same functions, presented differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison
&lt;/h2&gt;

&lt;p&gt;I put them next to each other:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Aurora &lt;code&gt;aurora_analytics&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;pg_duckdb (Docker)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wrapper node&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Custom Scan&lt;/code&gt; (provider &lt;code&gt;aurora_analytics&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Custom Scan (DuckDBScan)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scan&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;READ_PARQUET&lt;/code&gt; Filters: kind_id=1, Projections: production_year&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;READ_PARQUET&lt;/code&gt; Filters: kind_id=1, Projections: production_year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Group by&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PERFECT_HASH_GROUP_BY&lt;/code&gt; / &lt;code&gt;count_star()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PERFECT_HASH_GROUP_BY&lt;/code&gt; / &lt;code&gt;count_star()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sort&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ORDER_BY count_star() DESC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ORDER_BY count_star() DESC&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compression op&lt;/td&gt;
&lt;td&gt;&lt;code&gt;__internal_compress_integral_utinyint(#0, 1920)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;__internal_compress_integral_utinyint(#0, 1920)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decompression op&lt;/td&gt;
&lt;td&gt;&lt;code&gt;__internal_decompress_integral_integer(#0, 1920)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;__internal_decompress_integral_integer(#0, 1920)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They are the same plan. The detail that removes all doubt is the compression operator: both engines, independently, encoded &lt;code&gt;production_year&lt;/code&gt; with &lt;code&gt;__internal_compress_integral_utinyint(#0, 1920)&lt;/code&gt;. That is DuckDB's frame-of-reference integer compression, and 1920 is the minimum &lt;code&gt;production_year&lt;/code&gt; in the file — the base value DuckDB subtracts so the column fits in an unsigned byte. A separate implementation would not reproduce DuckDB's internal operator names and independently pick the same base constant from the&lt;br&gt;
data. Aurora’s plan exposes DuckDB operators, indicating that aurora_analytics uses DuckDB or DuckDB-derived components under its Foreign Data Wrapper.&lt;/p&gt;
&lt;h2&gt;
  
  
  How differently they package it
&lt;/h2&gt;

&lt;p&gt;It's the same engine, but opposite exposure. We can look at what each extension registers.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;pg_duckdb&lt;/code&gt; the whole DuckDB surface is SQL-visible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;dx&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;pg_duckdb&lt;/span&gt;
                                                                                                                                                      &lt;span class="n"&gt;Objects&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;extension&lt;/span&gt; &lt;span class="nv"&gt;"pg_duckdb"&lt;/span&gt;
                                                                                                                                                             &lt;span class="k"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;
&lt;span class="c1"&gt;--------------------------------------------------------------------&lt;/span&gt;
 &lt;span class="k"&gt;access&lt;/span&gt; &lt;span class="k"&gt;method&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;
 &lt;span class="k"&gt;cast&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unresolved_type&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;
 &lt;span class="k"&gt;cast&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;duckdb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unresolved_type&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
 &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full output shows 366 objects, including 235 function entries (overloads counted separately), 1 foreign-data wrapper (duckdb), and 23 type entries (including arrays). The remaining 107 objects include casts, operators, triggers, tables, and other objects. &lt;/p&gt;

&lt;p&gt;With &lt;code&gt;aurora_analytics&lt;/code&gt; the engine is sealed behind an FDW:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;dx&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;aurora_analytics&lt;/span&gt;
                                                                                                                                                      &lt;span class="n"&gt;Objects&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;extension&lt;/span&gt; &lt;span class="nv"&gt;"aurora_analytics"&lt;/span&gt;
                                                                                                                                                             &lt;span class="k"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;
&lt;span class="c1"&gt;--------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;foreign&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="n"&gt;wrapper&lt;/span&gt; &lt;span class="n"&gt;aurora_analytics_fdw&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;aurora_analytics_fdw_handler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;aurora_analytics_fdw_validator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="n"&gt;aurora_analytics_server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Aurora the DuckDB SQL dialect is not reachable — &lt;code&gt;read_parquet()&lt;/code&gt; as a function, list/struct literals, &lt;code&gt;QUALIFY&lt;/code&gt;, &lt;code&gt;approx_count_distinct&lt;/code&gt;, &lt;code&gt;::HUGEINT&lt;/code&gt; are all rejected by PostgreSQL’s SQL layer, because only the foreign-table scan is handed to DuckDB. With &lt;code&gt;pg_duckdb&lt;/code&gt; you can call the engine directly (&lt;code&gt;duckdb.query('…')&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;So, it's the same DuckDB underneath, but without exposing its functions. in summary:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;pg_duckdb&lt;/th&gt;
&lt;th&gt;aurora_analytics&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where it runs&lt;/td&gt;
&lt;td&gt;Any PostgreSQL (open source)&lt;/td&gt;
&lt;td&gt;Aurora PostgreSQL only (17.11+/18.6+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engine&lt;/td&gt;
&lt;td&gt;DuckDB&lt;/td&gt;
&lt;td&gt;DuckDB, embedded behind a foreign-data wrapper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lake interface&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;read_parquet('…') r&lt;/code&gt; with &lt;code&gt;r['col']&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CREATE FOREIGN TABLE … OPTIONS(format 'parquet')&lt;/code&gt;, schema inferred&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct DuckDB SQL&lt;/td&gt;
&lt;td&gt;Yes — &lt;code&gt;duckdb.query()&lt;/code&gt;, DuckDB dialect, extensions&lt;/td&gt;
&lt;td&gt;No — engine sealed; only foreign-table scans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accelerates your own PostgreSQL tables&lt;/td&gt;
&lt;td&gt;Yes — &lt;code&gt;SET duckdb.force_execution = true&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No — foreign (lake) tables only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write back to the lake&lt;/td&gt;
&lt;td&gt;Yes — &lt;code&gt;COPY (…) TO 's3://…'&lt;/code&gt; / &lt;code&gt;az://&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No — read-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Join live tables with the lake&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Iceberg / Delta&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;iceberg_scan&lt;/code&gt; / &lt;code&gt;delta_scan&lt;/code&gt; (DuckDB extensions)&lt;/td&gt;
&lt;td&gt;Iceberg via Glue catalog; no Delta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credentials&lt;/td&gt;
&lt;td&gt;DuckDB secret (connection string / keys)&lt;/td&gt;
&lt;td&gt;IAM role attached to the cluster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory / spill knob&lt;/td&gt;
&lt;td&gt;&lt;code&gt;duckdb.max_memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;aurora_analytics.query_mem&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observability&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;EXPLAIN&lt;/code&gt;, &lt;code&gt;duckdb.log_pg_explain&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;EXPLAIN&lt;/code&gt;, &lt;code&gt;aurora_analytics_stat_statements()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;AWS's acquisition of DuckDB should not affect the open-source DuckDB and &lt;code&gt;pg_duckdb&lt;/code&gt;. Here are some quotes from the Founder &amp;amp; CEO of MotherDuck (from his &lt;a href="https://motherduck.com/blog/duckdb-amazon/" rel="noopener noreferrer"&gt;blog post&lt;/a&gt;):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Some day, Amazon will likely release their own service based on DuckDB. After all, they’re not acquiring Duck Labs just because they love open source.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's what they did with &lt;code&gt;aurora_analytics&lt;/code&gt;, even though it's a proprietary foreign data wrapper in Amazon Aurora rather than a separate service.&lt;/p&gt;

&lt;p&gt;The core open-source engine remains fully open. Tigani notes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Duck Labs is being kept as a wholly-owned subsidiary, with its organization intact. The Duck Labs team will still be working in open source. The DuckDB Foundation has iron clad control over the DuckDB IP, and DuckDB license is going to stay permissively open.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because &lt;code&gt;pg_duckdb&lt;/code&gt; is governed under the DuckDB Foundation's intellectual property, it is similarly protected and will remain community-driven and open-source.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>duckdb</category>
      <category>aws</category>
      <category>azure</category>
    </item>
    <item>
      <title>How to Do Pagination Right: Row Comparison, Datatypes, and Index Range Scans</title>
      <dc:creator>Franck Pachot</dc:creator>
      <pubDate>Mon, 05 Oct 2026 13:46:52 +0000</pubDate>
      <link>https://dev.to/franckpachot/how-to-do-pagination-right-row-comparison-datatypes-and-index-range-scans-4n2g</link>
      <guid>https://dev.to/franckpachot/how-to-do-pagination-right-row-comparison-datatypes-and-index-range-scans-4n2g</guid>
      <description>&lt;p&gt;&lt;code&gt;OFFSET&lt;/code&gt; is simple to implement, but the database still needs to read and discard rows before reaching the requested page. Keyset pagination improves this by beginning after the last row from the previous page. Since different databases have various query planner optimizations and index access methods, the only way to ensure correctness is to examine the execution plan.&lt;/p&gt;

&lt;p&gt;The usual example, which I take from Vlad Mihalcea's &lt;a href="https://vladmihalcea.com/sql-seek-keyset-pagination/" rel="noopener noreferrer"&gt;blog post&lt;/a&gt;, orders posts by date and uses the identifier as a unique tie-breaker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next page should begin after the last row, and the most effective way to specify this in SQL is through row comparison:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;last_created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two details that make the difference between a direct index seek and an index scan with a filter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use a row comparison when the database can turn it into a composite index boundary.&lt;/li&gt;
&lt;li&gt;Pass values with exactly the right data types.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second point is easy to miss. A query might look flawless and produce correct results, yet still omit the second index column because it compares a &lt;code&gt;bigint&lt;/code&gt; with a &lt;code&gt;numeric&lt;/code&gt;. Whether you're a developer or an agent, always verify this by checking the execution plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing it on PostgreSQL
&lt;/h2&gt;

&lt;p&gt;This is the table and index used in the original example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;         &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;      &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_post_created_on_id&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The complete reproductions are available for &lt;a href="https://dbfiddle.uk/ZoKyTQZl" rel="noopener noreferrer"&gt;PostgreSQL 18&lt;/a&gt; and &lt;a href="https://dbfiddle.uk/M93CZhRz" rel="noopener noreferrer"&gt;PostgreSQL 15&lt;/a&gt; which I used in a &lt;a href="https://lnkd.in/p/eH8fkxr5" rel="noopener noreferrer"&gt;Linkedin discussion&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The best predicate: one correctly typed row comparison
&lt;/h3&gt;

&lt;p&gt;SQL can compare rows. The SQL standard defines the &lt;a href="https://jakewheat.github.io/sql-overview/sql-92-grammar.html#_8_2_comparison_predicate" rel="noopener noreferrer"&gt;comparison predicate&lt;/a&gt;, including greater than, with &lt;a href="https://jakewheat.github.io/sql-overview/sql-92-grammar.html#_7_1_row_value_constructor" rel="noopener noreferrer"&gt;row value constructors&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TIMING&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt;                      &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;           &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="s1"&gt;'2019-10-02 21:00:00'&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4951&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL puts the complete row comparison in the &lt;code&gt;Index Cond&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;
&lt;span class="k"&gt;Limit&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;39&lt;/span&gt;
  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="k"&gt;Only&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="n"&gt;idx_post_created_on_id&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Cond&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ROW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
                     &lt;span class="k"&gt;ROW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2019-10-02 21:00:00'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;without&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="s1"&gt;'4951'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;Heap&lt;/span&gt; &lt;span class="n"&gt;Fetches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
        &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Searches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;39&lt;/span&gt;
&lt;span class="n"&gt;Planning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="k"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;925&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;308&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what I want for pagination. The B-tree navigates to the composite value and reads the next 50 entries in index order. No sort or filter is needed. It seeks directly to the index entry of the first row to fetch, and it reads only what is necessary for the result.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;id&lt;/code&gt; is not optional here. Many posts can have the same timestamp. Pagination needs a total and stable order, so the last ordering column must make the key unique.&lt;/p&gt;

&lt;h3&gt;
  
  
  The exact OR expression is correct, but not as good on PostgreSQL
&lt;/h3&gt;

&lt;p&gt;A row comparison is equivalent, for non-null values, to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;    &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_created_on&lt;/span&gt;
&lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_created_on&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_id&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query returns the same rows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TIMING&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="s1"&gt;'2019-10-02 21:00:00'&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="s1"&gt;'2019-10-02 21:00:00'&lt;/span&gt;
      &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4951&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;
   &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But PostgreSQL keeps the expression as a filter, after scanning all rows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;Limit&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;39&lt;/span&gt;
  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="k"&gt;Only&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="n"&gt;idx_post_created_on_id&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
                  &lt;span class="s1"&gt;'2019-10-02 21:00:00'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;without&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
              &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
                   &lt;span class="s1"&gt;'2019-10-02 21:00:00'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;without&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="s1"&gt;'4951'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="n"&gt;Heap&lt;/span&gt; &lt;span class="n"&gt;Fetches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
        &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Searches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;39&lt;/span&gt;
&lt;span class="n"&gt;Planning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;133&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;049&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This small test does not show a performance problem because the first entries happen to qualify. The important difference is the plan operation: &lt;code&gt;Filter&lt;/code&gt; is not &lt;code&gt;Index Cond&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With many rows sharing the cursor timestamp, or with a cursor deep in the index, this form may scan and reject many entries before returning 50 rows. The row comparator gives PostgreSQL the composite boundary directly.&lt;/p&gt;

&lt;p&gt;Do not replace it with one of these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Too restrictive&lt;/span&gt;
&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;

&lt;span class="c1"&gt;-- Too broad&lt;/span&gt;
&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;

&lt;span class="c1"&gt;-- Also wrong: it loses older rows having a larger id&lt;/span&gt;
&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lexicographic order needs the equality prefix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;
&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;
&lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is logically equivalent to the row comparison, but database query planners rarely recognize &lt;code&gt;x &amp;lt; y OR x = y&lt;/code&gt;  as a single bound.&lt;/p&gt;

&lt;h2&gt;
  
  
  The datatype can silently remove part of the seek
&lt;/h2&gt;

&lt;p&gt;Now I change only one value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TIMING&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt;                      &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;            &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="s1"&gt;'2019-10-02 21:00:00'&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4951&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;         &lt;span class="c1"&gt;-------------&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The column is &lt;code&gt;bigint&lt;/code&gt;, but the value is &lt;code&gt;numeric&lt;/code&gt;. Here is the PostgreSQL plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;Limit&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;39&lt;/span&gt;
  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="k"&gt;Only&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="n"&gt;idx_post_created_on_id&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Cond&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;
                     &lt;span class="s1"&gt;'2019-10-02 21:00:00'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;without&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ROW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
                 &lt;span class="k"&gt;ROW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2019-10-02 21:00:00'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;without&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="s1"&gt;'4951'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;Heap&lt;/span&gt; &lt;span class="n"&gt;Fetches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
        &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Searches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;39&lt;/span&gt;
&lt;span class="n"&gt;Planning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;Buffers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="n"&gt;hit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;
&lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;288&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;107&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The significant lines are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Cond&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ROW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The index stores &lt;code&gt;id&lt;/code&gt; as &lt;code&gt;bigint&lt;/code&gt;, but the comparison casts the indexed column to &lt;code&gt;numeric&lt;/code&gt;. The second column is no longer usable as the &lt;code&gt;bigint&lt;/code&gt; B-tree boundary.&lt;/p&gt;

&lt;p&gt;PostgreSQL still extracts a safe condition from the leading column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_created_on&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It must be &lt;code&gt;&amp;lt;=&lt;/code&gt;, not &lt;code&gt;&amp;lt;&lt;/code&gt;, because rows at the same timestamp can qualify when their identifier is lower. PostgreSQL then evaluates the original row expression as a filter to preserve the correct result.&lt;/p&gt;

&lt;p&gt;The optimizer source calls this a &lt;a href="https://github.com/postgres/postgres/blob/REL_18_6/src/backend/optimizer/path/indxpath.c#L2692" rel="noopener noreferrer"&gt;lossy version of the row comparison&lt;/a&gt;. It means that the index condition identifies a superset of the required rows and the complete condition must be rechecked.&lt;/p&gt;

&lt;p&gt;The fix is only a cast, but it must be on the value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;last_created_on&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not cast the indexed column (except if it is indexed with an expression-based index):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Avoid this&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;last_created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a column defined as &lt;code&gt;bigint&lt;/code&gt;, the application should bind the cursor as &lt;code&gt;int8&lt;/code&gt;/&lt;code&gt;bigint&lt;/code&gt;, not as &lt;code&gt;numeric&lt;/code&gt; or &lt;code&gt;BigDecimal&lt;/code&gt; when that changes the PostgreSQL parameter type.&lt;/p&gt;

&lt;p&gt;A prepared statement makes the contract explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;PREPARE&lt;/span&gt; &lt;span class="n"&gt;next_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A prepared statement or function is the best way to ensure the execution plan uses the right data type, regardless of what is passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A test that exposes wasted index work
&lt;/h2&gt;

&lt;p&gt;The previous fiddle helps you inspect the plan, but its data doesn't magnify the difference. This creates one million rows, with 500,000 rows sharing each timestamp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;UNLOGGED&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;         &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;      &lt;span class="nb"&gt;text&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;
         &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;500000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'1 day'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="s1"&gt;'post '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;post_seek_idx&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;VACUUM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the three alternatives separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Exact composite boundary&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TIMING&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Only a leading-column boundary because bigint is compared with numeric&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TIMING&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Exact logical expansion, but commonly retained as a filter&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TIMING&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;
       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;
   &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first query can position near &lt;code&gt;(2024-01-01,99)&lt;/code&gt;. The other two may start near the beginning of the 500,000 entries for &lt;code&gt;2024-01-01&lt;/code&gt; and filter almost all of them.&lt;/p&gt;

&lt;p&gt;Always run this with your distribution and parameters. A query is not efficient because it contains &lt;code&gt;Index Scan&lt;/code&gt;. Inspect &lt;code&gt;Index Cond&lt;/code&gt;, &lt;code&gt;Filter&lt;/code&gt;, rows removed, buffers, and the number of entries actually read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which databases support an index for row comparison?
&lt;/h2&gt;

&lt;p&gt;The short answer depends on whether “support” means syntax or a direct composite index boundary.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Ordered row syntax&lt;/th&gt;
&lt;th&gt;Composite-index behavior for pagination&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Yes , with compatible B-tree operators and matching datatypes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YugabyteDB&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;The original PostgreSQL-style pattern is still not an exact DocDB seek. A new hash-aware form exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Yes when the row is the leftmost index prefix. Important limitation when it follows separate key predicates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MongoDB&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;The exact &lt;code&gt;$or&lt;/code&gt; can use two index scans combined with &lt;code&gt;SORT_MERGE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oracle&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Use scalar predicates. Typically a leading range plus filter, or bounded &lt;code&gt;UNION ALL&lt;/code&gt; branches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;The exact &lt;code&gt;OR&lt;/code&gt; can become multiple ranges in one ordered &lt;code&gt;Index Seek&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Accepting the syntax is not a sufficient test because the goal of this pagination filter is to read only what is necessary. The execution plan is the evidence. Here are some detailed tests&lt;/p&gt;

&lt;h2&gt;
  
  
  YugabyteDB: improved, but the old case still needs the workaround
&lt;/h2&gt;

&lt;p&gt;I wrote about this in &lt;a href="https://dev.to/yugabyte/efficient-pagination-in-yugabytedb-postgresql-4h5a"&gt;Efficient pagination in YugabyteDB &amp;amp; PostgreSQL&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The index was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;demo1_key_ts_id&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;demo1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In YugabyteDB, the first unspecified index column is hash-sharded, so this is effectively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="n"&gt;HASH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The elegant PostgreSQL query was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On YugabyteDB 2.8, the complete historical plan showed why &lt;code&gt;Index Cond&lt;/code&gt; was not sufficient evidence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;Limit&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;26446&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;024&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;26748&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;760&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;Nested&lt;/span&gt; &lt;span class="n"&gt;Loop&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="n"&gt;demo1_key_ts_id&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;demo1&lt;/span&gt;
              &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Cond&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                 &lt;span class="k"&gt;AND&lt;/span&gt;
                 &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ROW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
                  &lt;span class="k"&gt;ROW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2022-01-01 00:00:01+00'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="s1"&gt;'000fffff-ffff-ffff-ffff-ffffffffffff'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
              &lt;span class="k"&gt;Rows&lt;/span&gt; &lt;span class="n"&gt;Removed&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="k"&gt;Recheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;998000&lt;/span&gt;
&lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;159&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;26751&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;767&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The storage layer had not started at the complete row value. PostgreSQL rechecked and rejected 998,000 entries.&lt;/p&gt;

&lt;p&gt;The workaround was to provide a scalar bound for the first range column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
       &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The historical plan became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;Limit&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;769&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;305&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;666&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;Nested&lt;/span&gt; &lt;span class="n"&gt;Loop&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="n"&gt;demo1_key_ts_id&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;demo1&lt;/span&gt;
              &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Cond&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                 &lt;span class="k"&gt;AND&lt;/span&gt;
                 &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;
                  &lt;span class="s1"&gt;'2022-01-01 00:00:01+00'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
              &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
                  &lt;span class="s1"&gt;'2022-01-01 00:00:01+00'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                 &lt;span class="k"&gt;OR&lt;/span&gt;
                 &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
                   &lt;span class="s1"&gt;'2022-01-01 00:00:01+00'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                  &lt;span class="k"&gt;AND&lt;/span&gt;
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
                   &lt;span class="s1"&gt;'000fffff-ffff-ffff-ffff-ffffffffffff'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="n"&gt;Planning&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;039&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="n"&gt;Execution&lt;/span&gt; &lt;span class="nb"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;309&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;985&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What about now?&lt;/p&gt;

&lt;p&gt;YugabyteDB 2026.1 added a useful row boundary for hash indexes, but it is a different key shape. It starts with the hash code and includes a contiguous key prefix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;hc_rc_t1&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt;  &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;r1&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;r2&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="n"&gt;HASH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r1&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;hc_rc_t1&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'val'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TIMING&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SUMMARY&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;hc_rc_t1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yb_hash_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yb_hash_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The released 2026.1.2 regression plan is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;Limit&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="n"&gt;hc_rc_t1_pkey&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;hc_rc_t1&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;Cond&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ROW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yb_hash_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4624&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the leading hash code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;hc_rc_t1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the released test expects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;Limit&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="n"&gt;loops&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="n"&gt;Seq&lt;/span&gt; &lt;span class="n"&gt;Scan&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;hc_rc_t1&lt;/span&gt;
        &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ROW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;ROW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;Rows&lt;/span&gt; &lt;span class="n"&gt;Removed&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;Filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not fix the original portable predicate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Issue &lt;a href="https://github.com/yugabyte/yugabyte-db/issues/11794" rel="noopener noreferrer"&gt;#11794&lt;/a&gt; remains open, and a July 2026 issue still describes those row-comparison bounds as loose bounds requiring recheck. For the original index and ordering, I would still use the decomposed predicate and verify it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DIST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSTS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In YugabyteDB, check &lt;code&gt;Storage Index Rows Scanned&lt;/code&gt; and &lt;code&gt;Rows Removed by Index Recheck&lt;/code&gt;, not only &lt;code&gt;Index Cond&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  MySQL: native row comparison, with a prefix caveat
&lt;/h2&gt;

&lt;p&gt;MySQL 8.4 supports the same lexicographic syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;         &lt;span class="nb"&gt;bigint&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="nb"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;    &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;post_seek_i&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ENGINE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;InnoDB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'2026-01-01 12:00:00'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the row constructor covering the leftmost index prefix, the desired plan shape is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Limit: 50 row(s)
  -&amp;gt; Index range scan on post using post_seek_i
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But MySQL documents an important exception. With:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;post_tenant_seek_i&lt;/span&gt;
    &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the row starts at the second index part. MySQL may use only &lt;code&gt;tenant_id&lt;/code&gt;. Its documented example reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type: ref
key: PRIMARY
key_len: 4
Extra: Using where
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expanding the row comparison:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
       &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;allows the documented example to use all three key parts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type: range
key: PRIMARY
key_len: 12
Extra: Using where
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So MySQL supports index range access for row comparison, but do not generalize that to every position in a composite index. Check &lt;code&gt;access_type&lt;/code&gt;, &lt;code&gt;used_key_parts&lt;/code&gt;, actual rows, and &lt;code&gt;using_filesort&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://dev.mysql.com/doc/refman/8.4/en/row-constructor-optimization.html" rel="noopener noreferrer"&gt;MySQL row-constructor optimization&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  MongoDB: no tuple syntax, but two index seeks
&lt;/h2&gt;

&lt;p&gt;MongoDB has no equivalent of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created_on_-1__id_-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;use the exact lexicographic expansion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ISODate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-01-01T12:01:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;I&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;000000000000000000000004&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;$or&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;$lt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;$lt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;I&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is a complete small reproduction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertMany&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;000000000000000000000006&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;ISODate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-01-01T12:02:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;000000000000000000000005&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;ISODate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-01-01T12:01:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;000000000000000000000004&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;ISODate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-01-01T12:01:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;C&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;000000000000000000000003&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;ISODate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-01-01T12:01:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;D&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;000000000000000000000002&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;ISODate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-01-01T12:00:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;E&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;000000000000000000000001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nc"&gt;ISODate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-01-01T11:59:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;F&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created_on_-1__id_-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ISODate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-01-01T12:01:00Z&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;I&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ObjectId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;000000000000000000000004&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;$or&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;$lt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="na"&gt;$lt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;I&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created_on_-1__id_-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;explain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;executionStats&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MongoDB can plan this as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LIMIT
└─ FETCH
   └─ SORT_MERGE
      ├─ IXSCAN created_on_-1__id_-1
      └─ IXSCAN created_on_-1__id_-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SORT_MERGE&lt;/code&gt; merges two already ordered index streams. It is not a blocking in-memory &lt;code&gt;SORT&lt;/code&gt;. MongoDB 9.0 regression tests require this shape for sortable &lt;code&gt;$or&lt;/code&gt; queries.&lt;/p&gt;

&lt;p&gt;Explain output varies between the classic and slot-based engines. Inspect the complete result for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;executionStats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;nReturned&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;totalKeysExamined&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;totalDocsExamined&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;executionStages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;LIMIT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;inputStage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FETCH&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;inputStage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SORT_MERGE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;inputStages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;IXSCAN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;indexName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created_on_-1__id_-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;IXSCAN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;indexName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created_on_-1__id_-1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the plan structure and the decisive counters, not a captured complete JSON plan from a specific server. Run the reproduction on the target MongoDB version because the optimizer can also choose one broader index scan with a residual &lt;code&gt;$or&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;An array expression is not a replacement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;$expr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;$lt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$created_on&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;I&lt;/span&gt;&lt;span class="p"&gt;]]}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It compares a computed array and does not provide compound index bounds. Materializing an array field is also different because it becomes a multikey index.&lt;/p&gt;

&lt;h2&gt;
  
  
  Oracle: no ordered row comparison
&lt;/h2&gt;

&lt;p&gt;Oracle didn't implement the full SQL standard. it supports expression lists for equality and &lt;code&gt;IN&lt;/code&gt;, but not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;         &lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;    &lt;span class="n"&gt;varchar2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;post_pk&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;post_seek_i&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_created_on&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_created_on&lt;/span&gt;
       &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
           &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_created_on&lt;/span&gt;
           &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_id&lt;/span&gt;
       &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The redundant &lt;code&gt;created_on &amp;lt;= :last_created_on&lt;/code&gt; exposes a simple leading range to the optimizer. The desired shape is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WINDOW NOSORT STOPKEY
  TABLE ACCESS BY INDEX ROWID
    INDEX RANGE SCAN DESCENDING POST_SEEK_I

Predicate Information:
  access("CREATED_ON" &amp;lt;= :LAST_CREATED_ON)
  filter("CREATED_ON" &amp;lt; :LAST_CREATED_ON
         OR ("CREATED_ON" = :LAST_CREATED_ON
             AND "ID" &amp;lt; :LAST_ID))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a leading-column range followed by the exact filter, not one native two-column seek.&lt;/p&gt;

&lt;p&gt;If the filter reads too many entries, use two disjoint bounded branches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;same_time&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_created_on&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_id&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;older&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_created_on&lt;/span&gt;
  &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
  &lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;same_time&lt;/span&gt;
  &lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;older&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each branch can have its own index range. Fetching at most one page from each disjoint branch is sufficient to produce one global page.&lt;/p&gt;

&lt;p&gt;Capture actual operations and predicates with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="cm"&gt;/*+ GATHER_PLAN_STATISTICS */&lt;/span&gt;
       &lt;span class="p"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DBMS_XPLAN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DISPLAY_CURSOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ALLSTATS LAST +PREDICATE +OUTLINE'&lt;/span&gt;
&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The displayed Oracle plan above is the target shape, not a plan captured for this draft.&lt;/p&gt;

&lt;h2&gt;
  
  
  SQL Server: no tuple syntax, but multiple ranges in one seek
&lt;/h2&gt;

&lt;p&gt;T-SQL does not support an ordered row constructor. Use the exact disjunction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;IX_post_seek&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;page_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;last_created_on&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;last_created_on&lt;/span&gt;
       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;last_id&lt;/span&gt;
   &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SQL Server can compile the two disjoint ranges into one ordered &lt;code&gt;Index Seek&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Top
  Index Seek
    Object: IX_post_seek
    Ordered: True
    Scan Direction: FORWARD
    Seek Predicates:
      1. created_on &amp;lt; @last_created_on
      2. created_on = @last_created_on
         AND id &amp;lt; @last_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not native tuple syntax, but it has the required physical behavior. In the XML or graphical plan, confirm that both branches are under &lt;code&gt;Seek Predicates&lt;/code&gt;, not only under a residual &lt;code&gt;Predicate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For more columns, continue the equality prefixes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt;  &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plan shape above is documented from reproducible SQL Server investigations, but it was not captured on a SQL Server instance for this draft.&lt;/p&gt;

&lt;h2&gt;
  
  
  Direction, NULLs, and concurrent changes
&lt;/h2&gt;

&lt;p&gt;For a homogeneous order, the comparator reverses with the direction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ORDER BY a ASC,  b ASC  -&amp;gt; (a,b) &amp;gt; (:a,:b)
ORDER BY a DESC, b DESC -&amp;gt; (a,b) &amp;lt; (:a,:b)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For mixed directions, one tuple operator does not express the order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- ORDER BY created_on DESC, id ASC&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;
       &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
   &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The index must use the same mixed directions when the database supports them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pagination columns should preferably be &lt;code&gt;NOT NULL&lt;/code&gt;. A comparison with &lt;code&gt;NULL&lt;/code&gt; is not true, and databases may order nulls differently. If nulls are part of the application order, encode that order explicitly and index the same expression.&lt;/p&gt;

&lt;p&gt;Finally, keyset pagination does not freeze the dataset. Separate requests under &lt;code&gt;READ COMMITTED&lt;/code&gt; can observe inserts, deletes, or updates to ordering columns between pages. Decide whether this is acceptable for a live feed or whether all pages must use one consistent snapshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical rule
&lt;/h2&gt;

&lt;p&gt;For PostgreSQL, start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="n"&gt;last_created_on&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_on&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;page_size&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then read the plan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the complete row value should be in &lt;code&gt;Index Cond&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;there should be no residual cursor &lt;code&gt;Filter&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the index should provide the order&lt;/li&gt;
&lt;li&gt;the number of entries read should be close to the pagination size&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For other databases, preserve the same lexicographic semantics but use the form their optimizer can seek:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL: correctly typed row comparison&lt;/li&gt;
&lt;li&gt;YugabyteDB: verify storage pushdown, the original hash-plus-range case still needs the scalar range workaround&lt;/li&gt;
&lt;li&gt;MySQL: row comparison when it covers the leftmost index prefix, otherwise expand and inspect all used key parts&lt;/li&gt;
&lt;li&gt;MongoDB: two &lt;code&gt;$or&lt;/code&gt; branches and &lt;code&gt;SORT_MERGE&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Oracle: leading range plus filter, or two bounded branches&lt;/li&gt;
&lt;li&gt;SQL Server: expanded predicate compiled into multiple seek ranges.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SQL text is only the request. The execution plan tells you where pagination really starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does Hibernate generate the best for each database?
&lt;/h2&gt;

&lt;p&gt;Hibernate ORM does not use the dialect capability flags to choose the predicate shape for keyset pagination. For &lt;code&gt;ORDER BY created_on DESC, id DESC&lt;/code&gt; it generates &lt;code&gt;created_on &amp;lt; ? OR (created_on = ? AND id &amp;lt; ?)&lt;/code&gt;, i.e. exactly the "exact OR expression" shown above, on every database, including PostgreSQL where the row comparison &lt;code&gt;(created_on, id) &amp;lt; (?, ?)&lt;/code&gt; would yield an Index Cond composite boundary. &lt;/p&gt;

&lt;p&gt;Hibernate does contain dialect-aware machinery for row comparisons, true by default and overridden to false by Oracle, SQL Server, DB2, Sybase, HSQL and others, but it only applies to tuple/subquery comparisons written explicitly in HQL, not to keyset pagination.&lt;/p&gt;

&lt;p&gt;When a dialect must emulate a tuple comparison, the generation depends on an &lt;code&gt;indexOptimized&lt;/code&gt; flag: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the "Normal" form produces the top-level OR (a &amp;gt; 1 or a = 1 and b &amp;gt; 2), which defeats index seeks as we have seen above&lt;/li&gt;
&lt;li&gt;the "Optimized" form, selected by the dialects that lack row-value syntax (Oracle, SQL Server, DB2, Sybase, etc.), emits a &amp;gt;= 1 and not (a = 1 and b &amp;lt;= 2) — a top-level AND with a broadened leading bound that the optimizer can turn into an index range and then recheck, the same "leading range plus filter" that we have seen above.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, the key set pagination doesn't use this and then doenst have this optimization&lt;/p&gt;

&lt;p&gt;So on PostgreSQL and MySQL 8.0.30+, Hibernate's output is correct but uses the weaker filterable form instead of a composite index boundary. On SQL Server the expansion can still be compiled into multiple seek ranges, and on Oracle it may produce the scan-plus-filter plan unless you write the bound predicate yourself. &lt;/p&gt;

&lt;p&gt;In short, Hibernate guarantees correct results everywhere, but guarantees the optimal plan nowhere — the same 'verify with EXPLAIN' rule applies to ORM-generated SQL as to hand-written SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://vladmihalcea.com/sql-seek-keyset-pagination/" rel="noopener noreferrer"&gt;Vlad Mihalcea: SQL Seek Method or Keyset Pagination&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/18/functions-comparisons.html#ROW-WISE-COMPARISON" rel="noopener noreferrer"&gt;PostgreSQL row-constructor comparison&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.postgresql.org/docs/18/indexes-ordering.html" rel="noopener noreferrer"&gt;PostgreSQL indexes and &lt;code&gt;ORDER BY&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/postgres/postgres/blob/37bbf5bba09c4244d74f152557e69f73f2b6690e/src/backend/optimizer/path/indxpath.c#L3445-L3675" rel="noopener noreferrer"&gt;PostgreSQL optimizer implementation for row comparisons&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/yugabyte/yugabyte-db/issues/11794" rel="noopener noreferrer"&gt;YugabyteDB issue #11794&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.yugabyte.com/stable/releases/ybdb-releases/v2026.1/#v2026.1.2.0" rel="noopener noreferrer"&gt;YugabyteDB 2026.1.2 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/refman/8.4/en/row-constructor-optimization.html" rel="noopener noreferrer"&gt;MySQL row-constructor optimization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mongodb.com/docs/manual/reference/operator/query/or/" rel="noopener noreferrer"&gt;MongoDB &lt;code&gt;$or&lt;/code&gt; and indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlrf/Comparison-Conditions.html" rel="noopener noreferrer"&gt;Oracle comparison conditions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/language-elements/comparison-operators-transact-sql?view=sql-server-ver17" rel="noopener noreferrer"&gt;SQL Server comparison operators&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>sql</category>
      <category>database</category>
      <category>postgres</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
