<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>boringSQL | Supercharge your SQL &amp; PostgreSQL powers - expert</title>
    <subtitle>Learn practical SQL &amp; PostgreSQL techniques. Build rock-solid data systems with &#x27;boring&#x27; database solutions that deliver reliability without the drama.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://boringsql.com/tags/expert/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://boringsql.com"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-03-21T14:32:00+00:00</updated>
    <id>https://boringsql.com/tags/expert/atom.xml</id>
    <entry xml:lang="en">
        <title>pg_regresql: truly portable PostgreSQL statistics</title>
        <published>2026-03-21T14:32:00+00:00</published>
        <updated>2026-03-21T14:32:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/regresql-extension/"/>
        <id>https://boringsql.com/posts/regresql-extension/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/regresql-extension/">&lt;p&gt;The &lt;a href=&quot;&#x2F;posts&#x2F;portable-stats&#x2F;&quot;&gt;previous article&lt;&#x2F;a&gt; showed that PostgreSQL 18 makes optimizer statistics portable, but left one gap open:&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;It&#x27;s not worth trying to inject &lt;code&gt;relpages&lt;&#x2F;code&gt; as the planner checks the actual file size and scales it proportionally.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;The planner doesn&#x27;t trust &lt;code&gt;pg_class.relpages&lt;&#x2F;code&gt;. It calls &lt;code&gt;smgrnblocks()&lt;&#x2F;code&gt; to read the actual number of 8KB pages from disk. Your table is 74 pages on disk but &lt;code&gt;pg_class.relpages&lt;&#x2F;code&gt; says 123,513? The planner uses the ratio to scale &lt;code&gt;reltuples&lt;&#x2F;code&gt; down to match the actual file size. The selectivity ratios stay correct, plan shapes mostly survive, but the absolute cost estimates are off.&lt;&#x2F;p&gt;
&lt;p&gt;For debugging a single query, that&#x27;s usually fine. For automated regression testing where you compare &lt;code&gt;EXPLAIN&lt;&#x2F;code&gt; costs across runs, it breaks things. A cost threshold of 2× means something different when the baseline was computed from fake-scaled numbers.&lt;&#x2F;p&gt;
&lt;p&gt;As part of my work on &lt;a href=&quot;&#x2F;products&#x2F;regresql&#x2F;&quot;&gt;RegreSQL&lt;&#x2F;a&gt; I&#x27;m happy to announce &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;boringSQL&#x2F;regresql&#x2F;tree&#x2F;master&#x2F;pg_ext&quot;&gt;pg_regresql&lt;&#x2F;a&gt; extension which fixes this by hooking directly into the planner.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-the-planner-ignores-relpages&quot;&gt;Why the planner ignores relpages&lt;a class=&quot;zola-anchor&quot; href=&quot;#why-the-planner-ignores-relpages&quot; aria-label=&quot;Anchor link for: why-the-planner-ignores-relpages&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;When PostgreSQL&#x27;s planner calls &lt;code&gt;get_relation_info()&lt;&#x2F;code&gt; in &lt;code&gt;plancat.c&lt;&#x2F;code&gt;, it delegates to &lt;code&gt;estimate_rel_size()&lt;&#x2F;code&gt; which ends up in &lt;code&gt;table_block_relation_estimate_size()&lt;&#x2F;code&gt; in &lt;code&gt;tableam.c&lt;&#x2F;code&gt;. There, the actual page count comes from the storage manager:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;c&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;curpages &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; RelationGetNumberOfBlocks&lt;&#x2F;span&gt;&lt;span&gt;(rel);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The function then computes a tuple density from &lt;code&gt;pg_class&lt;&#x2F;code&gt; (&lt;code&gt;reltuples &#x2F; relpages&lt;&#x2F;code&gt;) and multiplies it by &lt;code&gt;curpages&lt;&#x2F;code&gt; to estimate tuples. So &lt;code&gt;pg_class.reltuples&lt;&#x2F;code&gt; isn&#x27;t ignored, it&#x27;s scaled to match the real file size. The reasoning is sound for normal operation: the catalog might be stale, but the file system is always current.&lt;&#x2F;p&gt;
&lt;p&gt;The same applies to indexes. The planner reads their actual sizes from disk too.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-pg-regresql-does&quot;&gt;What pg_regresql does&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-pg-regresql-does&quot; aria-label=&quot;Anchor link for: what-pg-regresql-does&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The extension hooks into &lt;code&gt;get_relation_info_hook&lt;&#x2F;code&gt;, a planner callback that runs after PostgreSQL reads the physical file stats. The hook replaces the file-based numbers with the values stored in &lt;code&gt;pg_class&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rel-&amp;gt;pages&lt;&#x2F;code&gt; ← &lt;code&gt;pg_class.relpages&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;rel-&amp;gt;tuples&lt;&#x2F;code&gt; ← &lt;code&gt;pg_class.reltuples&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;rel-&amp;gt;allvisfrac&lt;&#x2F;code&gt; ← &lt;code&gt;pg_class.relallvisible &#x2F; pg_class.relpages&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;It does the same for every index in &lt;code&gt;rel-&amp;gt;indexlist&lt;&#x2F;code&gt;. Pages and tuples for each index are overridden from the index&#x27;s own &lt;code&gt;pg_class&lt;&#x2F;code&gt; entry.&lt;&#x2F;p&gt;
&lt;p&gt;The guard conditions are simple: skip the override if &lt;code&gt;relpages == 0&lt;&#x2F;code&gt; (empty or never analyzed) or &lt;code&gt;reltuples == -1&lt;&#x2F;code&gt; (never analyzed). The hook only activates for tables that have been &lt;code&gt;ANALYZE&lt;&#x2F;code&gt;d or had statistics injected.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;installation&quot;&gt;Installation&lt;a class=&quot;zola-anchor&quot; href=&quot;#installation&quot; aria-label=&quot;Anchor link for: installation&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Build from source using PGXS:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;cd&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pg_ext&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;make&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;make&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; install&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It requires no GUCs, background workers, or shared memory.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;usage&quot;&gt;Usage&lt;a class=&quot;zola-anchor&quot; href=&quot;#usage&quot; aria-label=&quot;Anchor link for: usage&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Load the extension in your session:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LOAD&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;pg_regresql&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That&#x27;s it. Every &lt;code&gt;EXPLAIN&lt;&#x2F;code&gt; in this session will now use catalog statistics instead of file sizes. There are no functions to call, no tables to configure.&lt;&#x2F;p&gt;
&lt;p&gt;You can also load it per-database by adding it to &lt;code&gt;session_preload_libraries&lt;&#x2F;code&gt; in &lt;code&gt;postgresql.conf&lt;&#x2F;code&gt; or via &lt;code&gt;ALTER DATABASE&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ALTER DATABASE&lt;&#x2F;span&gt;&lt;span&gt; test_db &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; session_preload_libraries &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;pg_regresql&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;the-difference-it-makes&quot;&gt;The difference it makes&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-difference-it-makes&quot; aria-label=&quot;Anchor link for: the-difference-it-makes&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Using the same &lt;code&gt;test_orders&lt;&#x2F;code&gt; example from the &lt;a href=&quot;&#x2F;posts&#x2F;portable-stats&#x2F;&quot;&gt;previous article&lt;&#x2F;a&gt;: 10,000 actual rows, injected with production statistics claiming 50 million rows across 123,513 pages.&lt;&#x2F;p&gt;
&lt;p&gt;Without &lt;code&gt;pg_regresql&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;EXPLAIN &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; test_orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; created_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2024-06-01&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                                             QUERY PLAN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;----------------------------------------------------------------------------------------------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Index Scan using test_orders_created_at_idx on test_orders  (cost=0.29..153.21 rows=6340 width=26)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Index Cond: (created_at &amp;gt; &amp;#39;2024-06-01&amp;#39;::date)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The plan shape is correct (index scan thanks to the histogram), but the row estimate is 6,340. For a 50-million-row table where the filter covers roughly 10% of the histogram range, the expected estimate should be in the millions. The planner saw 74 real pages on disk, scaled &lt;code&gt;reltuples&lt;&#x2F;code&gt; down to ~30,000, then applied selectivity. The ratio is preserved but the absolute number is wrong.&lt;&#x2F;p&gt;
&lt;p&gt;With &lt;code&gt;pg_regresql&lt;&#x2F;code&gt;:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LOAD&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;pg_regresql&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;EXPLAIN &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; test_orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; created_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2024-06-01&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                                                QUERY PLAN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-----------------------------------------------------------------------------------------------------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Index Scan using test_orders_created_at_idx on test_orders  (cost=0.29..153212.27 rows=10791836 width=27)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Index Cond: (created_at &amp;gt; &amp;#39;2024-06-01&amp;#39;::date)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(2 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Cost numbers now reflect the full 50 million rows.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where-this-matters&quot;&gt;Where this matters&lt;a class=&quot;zola-anchor&quot; href=&quot;#where-this-matters&quot; aria-label=&quot;Anchor link for: where-this-matters&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Cost-based regression testing.&lt;&#x2F;strong&gt; If you&#x27;re comparing &lt;code&gt;EXPLAIN&lt;&#x2F;code&gt; costs between schema versions (which is what &lt;a href=&quot;&#x2F;products&#x2F;regresql&quot;&gt;RegreSQL&lt;&#x2F;a&gt; does), you need the absolute numbers to be stable and realistic. With the scaling behavior, your baseline costs are proportional to your test database size, not production. A migration that doubles a cost in production might show a 1.3× increase in CI because the scaled-down numbers compress the range.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Reproducing production plans on a laptop.&lt;&#x2F;strong&gt; Sometimes the plan shape itself changes depending on the absolute numbers. A query with multiple joins might get a different join order when the planner sees 50 million rows vs. 30,000 rows, because the cost crossover between hash join and nested loop depends on the absolute row count, not just the ratio.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Index-only scans.&lt;&#x2F;strong&gt; The &lt;code&gt;allvisfrac&lt;&#x2F;code&gt; (fraction of all-visible pages) matters for index-only scan costing. Without the hook, &lt;code&gt;allvisfrac&lt;&#x2F;code&gt; is computed from the real &lt;code&gt;relallvisible&lt;&#x2F;code&gt; catalog value divided by the real page count. With injected stats, &lt;code&gt;relallvisible&lt;&#x2F;code&gt; might be 120,000 but the real page count is 74, so the fraction clamps to 1.0 and the planner overestimates how cheap index-only scans are. The hook fixes this by using the injected &lt;code&gt;relpages&lt;&#x2F;code&gt; as the denominator.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-it-doesn-t-do&quot;&gt;What it doesn&#x27;t do&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-it-doesn-t-do&quot; aria-label=&quot;Anchor link for: what-it-doesn-t-do&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Column-level statistics and &lt;code&gt;ANALYZE&lt;&#x2F;code&gt; behavior are unchanged. The extension only affects how the planner reads table and index sizes. One thing worth noting: &lt;code&gt;EXPLAIN ANALYZE&lt;&#x2F;code&gt; will still show actual row counts from the real (small) data. The extension changes the planner&#x27;s cost estimates, not query execution.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-full-workflow&quot;&gt;The full workflow&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-full-workflow&quot; aria-label=&quot;Anchor link for: the-full-workflow&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Combining PostgreSQL 18&#x27;s portable statistics with &lt;code&gt;pg_regresql&lt;&#x2F;code&gt;, the full workflow looks like this:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 1. dump schema and statistics from production&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;pg_dump&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --schema-only -d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; production_db&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; schema.sql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;pg_dump&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --statistics-only -d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; production_db&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; stats.sql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 2. create test database&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;createdb&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; test_db&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;psql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; test_db&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; schema.sql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 3. load minimal fixture data (optional)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;psql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; test_db&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; fixtures.sql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 4. inject production statistics&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;psql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; test_db&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; stats.sql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 5. install pg_regresql and prevent stats from being overwritten&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;psql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; test_db&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;lt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;SQL&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;ALTER DATABASE test_db SET session_preload_libraries = &amp;#39;pg_regresql&amp;#39;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;ALTER TABLE orders SET (autovacuum_enabled = false);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;-- repeat for other tables&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;SQL&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 6. reconnect and verify (plans now match production)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;psql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; test_db&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -c&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;EXPLAIN SELECT * FROM orders WHERE status = &amp;#39;pending&amp;#39;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;compatibility&quot;&gt;Compatibility&lt;a class=&quot;zola-anchor&quot; href=&quot;#compatibility&quot; aria-label=&quot;Anchor link for: compatibility&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The extension works with PostgreSQL 13 through 18. The portable statistics functions (&lt;code&gt;pg_restore_relation_stats&lt;&#x2F;code&gt;, &lt;code&gt;pg_restore_attribute_stats&lt;&#x2F;code&gt;) require PostgreSQL 18, but &lt;code&gt;pg_regresql&lt;&#x2F;code&gt; works with any method of writing to &lt;code&gt;pg_class&lt;&#x2F;code&gt;, including direct catalog updates on older versions.&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL 19 will need a small update: the &lt;code&gt;get_relation_info_hook&lt;&#x2F;code&gt; used by pg_regresql has been &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;git.postgresql.org&#x2F;gitweb&#x2F;?p=postgresql.git;a=commit;h=91f33a2ae92&quot;&gt;replaced&lt;&#x2F;a&gt; with &lt;code&gt;build_simple_rel_hook&lt;&#x2F;code&gt;. The new hook runs slightly later with different arguments, but the override logic stays the same.&lt;&#x2F;p&gt;
&lt;div class=&quot;callout&quot;&gt;
&lt;strong&gt;Don&#x27;t use this in production.&lt;&#x2F;strong&gt; The extension makes the planner ignore reality. That&#x27;s exactly what you want for testing with injected statistics. In production, you want the planner to see the actual file sizes so it can adapt to data growth, bloat, and vacuum activity. Keep &lt;code&gt;pg_regresql&lt;&#x2F;code&gt; in your dev&#x2F;test and CI databases.
&lt;&#x2F;div&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Production query plans without production data</title>
        <published>2026-03-08T22:05:00+00:00</published>
        <updated>2026-03-08T22:05:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/portable-stats/"/>
        <id>https://boringsql.com/posts/portable-stats/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/portable-stats/">&lt;p&gt;In the &lt;a href=&quot;&#x2F;posts&#x2F;postgresql-statistics&#x2F;&quot;&gt;previous article&lt;&#x2F;a&gt; we covered how the PostgreSQL planner reads &lt;code&gt;pg_class&lt;&#x2F;code&gt; and &lt;code&gt;pg_statistic&lt;&#x2F;code&gt; to estimate row counts, choose join strategies, and decide whether an index scan is worth it. The message was clear: when statistics are wrong, everything else goes with it.&lt;&#x2F;p&gt;
&lt;div class=&quot;sidenote&quot;&gt;Streaming replication provides bit-to-bit replication, so all replicas share the same statistics with primary server.&lt;&#x2F;div&gt;
But there was one thing we didn&#x27;t talk about. Statistics are specific to the database cluster that generated them. The primary way to populate them is `ANALYZE` which requires the actual data.
&lt;p&gt;PostgreSQL 18 changed that. Two new functions: &lt;code&gt;pg_restore_relation_stats&lt;&#x2F;code&gt; and &lt;code&gt;pg_restore_attribute_stats&lt;&#x2F;code&gt; write numbers directly into the catalog tables. Combined with &lt;code&gt;pg_dump --statistics-only&lt;&#x2F;code&gt;, you can treat optimizer statistics as a deployable artifact. Compact, portable, plain SQL.&lt;&#x2F;p&gt;
&lt;p&gt;The feature was &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.cybertec-postgresql.com&#x2F;en&#x2F;preserve-optimizer-statistics-during-major-upgrades-with-postgresql-v18&#x2F;&quot;&gt;driven by the upgrade use case&lt;&#x2F;a&gt;. In the past, major version upgrades used to leave &lt;code&gt;pg_statistic&lt;&#x2F;code&gt; empty, forcing you to run &lt;code&gt;ANALYZE&lt;&#x2F;code&gt;. Which might take hours on large clusters. With PostgreSQL 18 upgrades now transfer statistics automatically. But that&#x27;s just the beginning. The same logic lets you export statistics from production and inject them anywhere - test database, local debugging, or as part of CI pipelines.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-problem&quot;&gt;The problem&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-problem&quot; aria-label=&quot;Anchor link for: the-problem&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Your CI database has 1,000 rows. Production has 50 million. The planner makes completely different decisions for each. Running &lt;code&gt;EXPLAIN&lt;&#x2F;code&gt; in CI tells you nothing about the production plan. This is the core premise behind &lt;a href=&quot;&#x2F;products&#x2F;regresql&quot;&gt;RegreSQL&lt;&#x2F;a&gt;. Catching query plan regressions in CI is far more reliable when the planner sees production-scale statistics.&lt;&#x2F;p&gt;
&lt;p&gt;Same applies to &lt;strong&gt;debugging&lt;&#x2F;strong&gt;. A query is slow in production and you want to reproduce the plan locally, but your database has different statistics, and planner chooses the predictable path. Porting production stats can provide you that snapshot of thinking planner has to do in production, without actually going to production.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pg-restore-relation-stats&quot;&gt;pg_restore_relation_stats&lt;a class=&quot;zola-anchor&quot; href=&quot;#pg-restore-relation-stats&quot; aria-label=&quot;Anchor link for: pg-restore-relation-stats&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The first of function behind portable PostgreSQL statistics is &lt;code&gt;pg_restore_relation_stats&lt;&#x2F;code&gt;. It writes table-level data directly into &lt;code&gt;pg_class&lt;&#x2F;code&gt; in form of variadic name&#x2F;value pairs.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; pg_restore_relation_stats(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;schemaname&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;public&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;relname&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;orders&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;relpages&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;123513&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;integer&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;reltuples&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;50000000&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;real&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;relallvisible&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;123513&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;integer&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;relallfrozen&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;120000&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;integer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;But that&#x27;s just an example. Let&#x27;s modify some real statistics to see the full value. We will create a small table, inject fake production-like statistics and watch the planner to change its mind.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; test_orders&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    customer_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;integer NOT NULL&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    amount &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;numeric&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;10&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; NOT NULL&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    status text NOT NULL DEFAULT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;pending&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    created_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;date NOT NULL DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; CURRENT_DATE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INSERT INTO&lt;&#x2F;span&gt;&lt;span&gt; test_orders (customer_id, amount, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;status&lt;&#x2F;span&gt;&lt;span&gt;, created_at)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    (random()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 9999&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span&gt;)::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;int&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    (random()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 5000&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 5&lt;&#x2F;span&gt;&lt;span&gt;)::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;numeric&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;10&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ARRAY&lt;&#x2F;span&gt;&lt;span&gt;[&amp;#39;pending&amp;#39;,&amp;#39;shipped&amp;#39;,&amp;#39;delivered&amp;#39;,&amp;#39;cancelled&amp;#39;])[floor(random()*4+1)::int],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;2024-01-01&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;date +&lt;&#x2F;span&gt;&lt;span&gt; (random()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 365&lt;&#x2F;span&gt;&lt;span&gt;)::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;int&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; generate_series&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;10000&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; test_orders (created_at);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; test_orders (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;status&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ANALYZE test_orders;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;When you check the current statistics, it has predictable data.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; relname, relpages, reltuples&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; pg_class &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; relname &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;test_orders&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   relname   | relpages | reltuples&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-------------+----------+-----------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; test_orders |       74 |     10000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(1 row)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;With 10,000 rows across 74 pages, the planner picks a sequential scan.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;EXPLAIN &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; test_orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; created_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2024-06-01&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                           QUERY PLAN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-----------------------------------------------------------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Seq Scan on test_orders  (cost=0.00..199.00 rows=5891 width=26)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Filter: (created_at &amp;gt; &amp;#39;2024-06-01&amp;#39;::date)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(2 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now inject production-scale table stats:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; pg_restore_relation_stats(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;schemaname&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;public&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;relname&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;test_orders&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;relpages&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;123513&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;integer&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;reltuples&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;50000000&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;real&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;relallvisible&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;123513&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;integer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And you might be surprised by the result.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;EXPLAIN &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; test_orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; created_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2024-06-01&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                            QUERY PLAN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;------------------------------------------------------------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Seq Scan on test_orders  (cost=0.00..448.45 rows=17649 width=26)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Filter: (created_at &amp;gt; &amp;#39;2024-06-01&amp;#39;::date)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The planner is still using the sequential plan. Only the estimated number of rows has changed. Why? If you remember from previous article, it&#x27;s where column level statistics come into play. Histogram bounds still match the original 10,000 rows we inserted.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pg-restore-attribute-stats&quot;&gt;pg_restore_attribute_stats&lt;a class=&quot;zola-anchor&quot; href=&quot;#pg-restore-attribute-stats&quot; aria-label=&quot;Anchor link for: pg-restore-attribute-stats&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;This function writes column-level statistics into &lt;code&gt;pg_statistic&lt;&#x2F;code&gt; the same catalog that &lt;a href=&quot;&#x2F;posts&#x2F;postgresql-statistics&#x2F;#how-analyze-works&quot;&gt;ANALYZE populates&lt;&#x2F;a&gt; with &lt;a href=&quot;&#x2F;posts&#x2F;postgresql-statistics&#x2F;#pg_statistic-via-pg_stats---column-level-stats&quot;&gt;MCVs, histograms, and correlation&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;In previous section, we left the planner stuck on a sequential scan despite believing the table has 50 million rows. The missing piece is column-level statistics. Let&#x27;s pick up where we left off and inject histogram bounds for &lt;code&gt;created_at&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; pg_restore_attribute_stats(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;schemaname&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;public&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;relname&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;test_orders&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;attname&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;created_at&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;inherited&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, false::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;boolean&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;null_frac&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;real&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;avg_width&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;4&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;integer&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;n_distinct&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;05&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;real&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;histogram_bounds&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;{2019-01-01,2019-07-01,2020-01-01,2020-07-01,2021-01-01,2021-07-01,2022-01-01,2022-07-01,2023-01-01,2023-07-01,2024-01-01}&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;text&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;correlation&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;98&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;real&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now the planner knows the data spans 5 years. A query filtering on the last 6 months of 2024 covers a narrow slice.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;EXPLAIN SELECT * FROM test_orders WHERE created_at &amp;gt; &amp;#39;2024-06-01&amp;#39;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                                             QUERY PLAN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;----------------------------------------------------------------------------------------------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Index Scan using test_orders_created_at_idx on test_orders  (cost=0.29..153.21 rows=6340 width=26)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Index Cond: (created_at &amp;gt; &amp;#39;2024-06-01&amp;#39;::date)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;div class=&quot;sidenote&quot;&gt;
Histogram bounds divide the non-MCV portion of the data into equal-population buckets. If &lt;code&gt;most_common_vals&lt;&#x2F;code&gt; accounts for most of the data, the histogram covers only the remaining tail. The number of buckets is controlled by &lt;code&gt;default_statistics_target&lt;&#x2F;code&gt; (default 100, meaning 101 bounds).
&lt;&#x2F;div&gt;
&lt;p&gt;And that&#x27;s a plan flip! The histogram tells the planner the data spans 2019–2024, so &lt;code&gt;&amp;gt; &#x27;2024-06-01&#x27;&lt;&#x2F;code&gt; matches a narrow tail. A small fraction of 50 million rows. The index scan that was ignored before is now the obvious choice. Table-level stats set the scale, column-level stats shaped the selectivity, and together they changed the plan.&lt;&#x2F;p&gt;
&lt;div class=&quot;callout&quot;&gt;
The &lt;code&gt;correlation&lt;&#x2F;code&gt; statistic tells the planner how closely the physical row order matches the column&#x27;s sort order. A value near 1.0 means sequential access patterns - making &lt;a href=&quot;&#x2F;posts&#x2F;postgresql-statistics&#x2F;#correlation-and-index-scan-cost&quot;&gt;index scan cheaper&lt;&#x2F;a&gt; because the next row is likely on the same or adjacent page. For time-series data like &lt;code&gt;created_at&lt;&#x2F;code&gt; where rows are inserted chronologically, correlation is typically very high.
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;injecting-a-skewed-distribution&quot;&gt;Injecting a skewed distribution&lt;a class=&quot;zola-anchor&quot; href=&quot;#injecting-a-skewed-distribution&quot; aria-label=&quot;Anchor link for: injecting-a-skewed-distribution&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The same function handles &lt;a href=&quot;&#x2F;posts&#x2F;postgresql-statistics&#x2F;#selectivity-in-action&quot;&gt;MCV lists&lt;&#x2F;a&gt;. In production, your &lt;code&gt;status&lt;&#x2F;code&gt; column isn&#x27;t uniform, 95% of orders are delivered, 1.5% are pending.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; pg_restore_attribute_stats(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;schemaname&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;public&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;relname&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;test_orders&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;attname&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;status&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;inherited&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, false::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;boolean&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;null_frac&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;real&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;avg_width&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;9&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;integer&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;n_distinct&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;5&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;real&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;most_common_vals&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;{delivered,shipped,cancelled,pending,returned}&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;text&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;most_common_freqs&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;{0.95,0.015,0.015,0.015,0.005}&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;real&lt;&#x2F;span&gt;&lt;span&gt;[]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You can see&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;EXPLAIN &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; test_orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE status =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;pending&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                                      QUERY PLAN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;---------------------------------------------------------------------------------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Bitmap Heap Scan on test_orders  (cost=8.93..90.42 rows=599 width=27)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Recheck Cond: (status = &amp;#39;pending&amp;#39;::text)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   -&amp;gt;  Bitmap Index Scan on test_orders_status_idx  (cost=0.00..8.78 rows=599 width=0)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         Index Cond: (status = &amp;#39;pending&amp;#39;::text)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(4 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and compare it with&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;EXPLAIN &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; test_orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE status =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;delivered&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                            QUERY PLAN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;------------------------------------------------------------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Seq Scan on test_orders  (cost=0.00..448.45 rows=28458 width=27)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Filter: (status = &amp;#39;delivered&amp;#39;::text)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(2 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Same column, same operator, different plans. The planner uses a bitmap index scan for &lt;code&gt;pending&lt;&#x2F;code&gt; (1.5% rare enough to justify the index) and a sequential scan for &lt;code&gt;delivered&lt;&#x2F;code&gt; (95% being most of the table). The selectivity ratios from the MCV list drive the plan choice.&lt;&#x2F;p&gt;
&lt;div class=&quot;callout&quot;&gt;
You might have noticed the row estimates (599 and 28,458) are lower than you&#x27;d expect for a 50-million-row table. The planner checks the actual physical file size. Our table is only 74 pages on disk, not the 123,513 we injected. Hence the planner scales &lt;code&gt;reltuples&lt;&#x2F;code&gt; and &lt;code&gt;relpages&lt;&#x2F;code&gt; down proportionally. The absolute numbers shrink, but the &lt;i&gt;ratios&lt;&#x2F;i&gt; between them stay correct, and it&#x27;s the ratios that determine plan shape. When you use &lt;code&gt;pg_dump --statistics-only&lt;&#x2F;code&gt; in practice, you&#x27;re typically restoring into a database with comparable data volume, so the estimates align naturally.
&lt;&#x2F;div&gt;
&lt;div class=&quot;visualizer-banner&quot;&gt;
    &lt;div class=&quot;visualizer-banner__preview&quot; style=&quot;grid-template-columns: 1fr; gap: 2px; width: 48px; padding: 0.4rem;&quot;&gt;
        &lt;div style=&quot;height:10px; border-radius:2px; background:var(--viz-page-header);&quot;&gt;&lt;&#x2F;div&gt;
        &lt;div style=&quot;height:6px; width:60%; margin:0 auto; border-radius:0 0 2px 2px; background:var(--viz-page-lp);&quot;&gt;&lt;&#x2F;div&gt;
        &lt;div style=&quot;height:10px; border-radius:2px; background:var(--viz-page-header);&quot;&gt;&lt;&#x2F;div&gt;
    &lt;&#x2F;div&gt;
    &lt;div class=&quot;visualizer-banner__content&quot;&gt;
        &lt;strong&gt;pg_regresql extension&lt;&#x2F;strong&gt;
        &lt;p&gt;The &lt;code&gt;pg_regresql&lt;&#x2F;code&gt; extension fixes this scaling problem. It hooks into the planner to trust the injected &lt;code&gt;relpages&lt;&#x2F;code&gt; value instead of reading the physical file size, so cost estimates match production even when your test database is tiny.&lt;&#x2F;p&gt;
        &lt;a href=&quot;&#x2F;posts&#x2F;regresql-extension&#x2F;&quot; class=&quot;visualizer-banner__button&quot;&gt;Read more&lt;&#x2F;a&gt;
    &lt;&#x2F;div&gt;
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;pg-dump&quot;&gt;pg_dump&lt;a class=&quot;zola-anchor&quot; href=&quot;#pg-dump&quot; aria-label=&quot;Anchor link for: pg-dump&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The functions we covered are the mechanics. For operational use &lt;code&gt;pg_dump&lt;&#x2F;code&gt; provides everything you need. PostgreSQL 18 added three flags.&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Flag&lt;&#x2F;th&gt;&lt;th&gt;Effect&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--statistics&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;dump the statistics (you have to request it explicitely)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--statistics-only&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;dump only the statistics, not schema or data&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;--no-statistics&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;do not dump statistics&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;When you export the statistics for your production database&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;pg_dump --statistics-only -d production_db &amp;gt; stats.sql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;you will see the output is series of &lt;code&gt;SELECT pg_restore_relation_stats(...)&lt;&#x2F;code&gt; and &lt;code&gt;SELECT pg_restore_attribute_stats(...)&lt;&#x2F;code&gt; calls. Exactly as we explained above.&lt;&#x2F;p&gt;
&lt;p&gt;The full workflow to turn your production data into testable plans might look like this:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 1. dump schema from production&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;pg_dump&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --schema-only -d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; production_db&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; schema.sql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 2. dump statistics from production&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;pg_dump&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --statistics-only -d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; production_db&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; stats.sql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 3. create test database with schema&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;createdb&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; test_db&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;psql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; test_db&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; schema.sql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 4. load fixture data (optional; masked, minimal)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;psql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; test_db&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; fixtures.sql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 5. inject production statistics&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;psql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; test_db&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; stats.sql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 6. query plans now match production&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;psql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; test_db&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -c&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;EXPLAIN SELECT * FROM test_orders WHERE status = &amp;#39;pending&amp;#39;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;div class=&quot;callout&quot;&gt;
Statistics dumps are tiny. A database with hundreds of tables and thousands of columns produces a statistics dump under 1MB. The production data might be hundreds of GB. The statistics that describe it fit in a text file.
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;keeping-injected-statistics-alive&quot;&gt;Keeping injected statistics alive&lt;a class=&quot;zola-anchor&quot; href=&quot;#keeping-injected-statistics-alive&quot; aria-label=&quot;Anchor link for: keeping-injected-statistics-alive&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Now you might ask yourself, where&#x27;s the catch? And there&#x27;s a big one, the autovacuum will eventually kick in and run &lt;code&gt;ANALYZE&lt;&#x2F;code&gt;. Which will overwrite your injected statistics with real numbers and you are back where you started.&lt;&#x2F;p&gt;
&lt;p&gt;To prevent this, disable autovacuum analyze on the tables you&#x27;ve injected.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- disable autovacuum&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ALTER TABLE&lt;&#x2F;span&gt;&lt;span&gt; test_orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; (autovacuum_enabled &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; false);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- or set analyze threshold so high it nevers kicks-in&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ALTER TABLE&lt;&#x2F;span&gt;&lt;span&gt; test_orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; (autovacuum_analyze_threshold &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2147483647&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;div class=&quot;callout&quot;&gt;
&lt;strong&gt;Be careful here.&lt;&#x2F;strong&gt;
&lt;p&gt;
 If you&#x27;re also writing data to these tables in dev:  running migrations, loading fixtures, testing inserts, the injected statistics will drift further from reality with every write. The planner will plan based on a production distribution that no longer reflects the local data. &lt;&#x2F;p&gt;
&lt;p&gt;For read-only query plan testing this is exactly what you want. For integration tests that modify data, you may need to re-inject statistics after each test run.&lt;&#x2F;p&gt;
&lt;p&gt;And please, never ever do this in production!&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;what-s-not-covered&quot;&gt;What&#x27;s not covered?&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-s-not-covered&quot; aria-label=&quot;Anchor link for: what-s-not-covered&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;As we have seen earlier, it&#x27;s not worth trying to inject &lt;code&gt;relpages&lt;&#x2F;code&gt; as the planner checks the actual file size and scales it proportationally. This limits the number of absolute rows planner might estimate. I.e. to get comparable numbers to production environment you still would have to create comparable data volume (which isn&#x27;t a problem when talking about the primary use case of this feature - restoring backups).&lt;&#x2F;p&gt;
&lt;p&gt;It&#x27;s also worth to note that &lt;code&gt;CREATE STATISTICS&lt;&#x2F;code&gt; used for &lt;a href=&quot;&#x2F;posts&#x2F;postgresql-statistics&#x2F;#extended-statistics&quot;&gt;multivariate correlations, distinct counts across column groups and MCV lists for column combinations&lt;&#x2F;a&gt; are not covered within PostgreSQL 18.  Those still require &lt;code&gt;ANALYZE&lt;&#x2F;code&gt; after restore. PostgreSQL 19 will close this gap with &lt;code&gt;pg_restore_extended_stats()&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;security&quot;&gt;Security&lt;a class=&quot;zola-anchor&quot; href=&quot;#security&quot; aria-label=&quot;Anchor link for: security&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The restore functions require the &lt;code&gt;MAINTAIN&lt;&#x2F;code&gt; privilege on the target table. This is the same privilege needed for &lt;code&gt;ANALYZE&lt;&#x2F;code&gt;, &lt;code&gt;VACUUM&lt;&#x2F;code&gt;, &lt;code&gt;REINDEX&lt;&#x2F;code&gt;, and &lt;code&gt;CLUSTER&lt;&#x2F;code&gt; as it was &lt;a href=&quot;&#x2F;posts&#x2F;postgresql-predefined-roles&#x2F;&quot;&gt;introduced in PostgreSQL 17&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The easiest way to grant it for automation:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GRANT&lt;&#x2F;span&gt;&lt;span&gt; pg_maintain &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; ci_service_account;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This grants &lt;code&gt;MAINTAIN&lt;&#x2F;code&gt; on all tables in the database. Enough for a CI pipeline to inject statistics without needing superuser.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Instant database clones with PostgreSQL 18</title>
        <published>2025-12-22T23:53:16+00:00</published>
        <updated>2025-12-22T23:53:16+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/instant-database-clones/"/>
        <id>https://boringsql.com/posts/instant-database-clones/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/instant-database-clones/">&lt;p&gt;Have you ever watched a &lt;a href=&quot;&#x2F;posts&#x2F;how-not-to-change-postgresql-column-type&#x2F;&quot;&gt;long running migration script&lt;&#x2F;a&gt;, wondering if it&#x27;s about
to wreck your data? Or wish you can &quot;just&quot; spin a fresh copy of database for
each test run? Or wanted to have reproducible snapshots to reset between
runs of your test suite, (and yes, because you are reading boringSQL) needed
to reset the learning environment?&lt;&#x2F;p&gt;
&lt;p&gt;When your database is a few megabytes, &lt;code&gt;pg_dump&lt;&#x2F;code&gt; and restore works fine. But
what happens when you&#x27;re dealing with hundreds of megabytes&#x2F;gigabytes - or more?
Suddenly &quot;just make a copy&quot; becomes a burden.&lt;&#x2F;p&gt;
&lt;p&gt;You&#x27;ve probably noticed that PostgreSQL connects to &lt;code&gt;template1&lt;&#x2F;code&gt; by default. What
you might have missed is that there&#x27;s a whole templating system hiding in plain
sight. Every time you run&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;CREATE DATABASE dbname;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;PostgreSQL quietly clones standard system database &lt;code&gt;template1&lt;&#x2F;code&gt; behind the
scenes. Making it same as if you would use&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;CREATE DATABASE dbname TEMPLATE template1;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The real power comes from the fact that you can replace &lt;code&gt;template1&lt;&#x2F;code&gt; with any
database. You can find more at &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;manage-ag-templatedbs.html&quot;&gt;Template Database
documentation&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;In this article, we will cover a few tweaks that turn this templating system
into an instant, zero-copy database cloning machine.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;create-database-strategy&quot;&gt;CREATE DATABASE ... STRATEGY&lt;a class=&quot;zola-anchor&quot; href=&quot;#create-database-strategy&quot; aria-label=&quot;Anchor link for: create-database-strategy&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Before PostgreSQL 15, when you created a new database from a template, it
operated strictly on the file level. This was effective, but to make it
reliable, Postgres had to flush all pending operations to disk (using
&lt;code&gt;CHECKPOINT&lt;&#x2F;code&gt;) before taking a consistent snapshot. This created a massive I&#x2F;O
spike - a &quot;Checkpoint Storm&quot; - that could stall your production traffic.&lt;&#x2F;p&gt;
&lt;p&gt;Version 15 of PostgreSQL introduced new parameter &lt;code&gt;CREATE DATABASE ... STRATEGY = [strategy]&lt;&#x2F;code&gt; and at the same time changed the default behaviour how the new
databases are created from templates. The new default become &lt;code&gt;WAL_LOG&lt;&#x2F;code&gt; which
copies block-by-block via the Write-Ahead Log (WAL), making I&#x2F;O sequential (and
much smoother) — operations that also &lt;a href=&quot;&#x2F;posts&#x2F;explain-buffers&#x2F;&quot;&gt;show up in EXPLAIN buffer statistics&lt;&#x2F;a&gt; — and support for concurrency without facing latency spike. This
prevented the need to CHECKPOINT but made the database cloning operation
potentially significantly slower. For an empty &lt;code&gt;template1&lt;&#x2F;code&gt;, you won&#x27;t notice the
difference. But if you try to clone a 500GB database using WAL_LOG, you are
going to be waiting a long time.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;code&gt;STRATEGY&lt;&#x2F;code&gt; parameter allows us to switch back to the original method
&lt;code&gt;FILE_COPY&lt;&#x2F;code&gt; to keep the behaviour, and speed. And since PostgreSQL 18, this
opens the whole new set of options.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;file-copy&quot;&gt;FILE_COPY&lt;a class=&quot;zola-anchor&quot; href=&quot;#file-copy&quot; aria-label=&quot;Anchor link for: file-copy&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Because the &lt;code&gt;FILE_COPY&lt;&#x2F;code&gt; strategy is a proxy to operating system file operations,
we can change how the OS handles those files.&lt;&#x2F;p&gt;
&lt;p&gt;When using standard file system (like &lt;code&gt;ext4&lt;&#x2F;code&gt;), PostgreSQL reads every byte of
the source file and writes it to a new location. It&#x27;s a physical copy. However
starting with PostgreSQL 18 - &lt;code&gt;file_copy_method&lt;&#x2F;code&gt; gives you options to switch
that logic; while default option remains &lt;code&gt;copy&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;With modern filesystems (like ZFS, XFS with reflinks, APFS, etc.) you can switch
it to &lt;code&gt;clone&lt;&#x2F;code&gt; and leverage &lt;code&gt;CLONE&lt;&#x2F;code&gt; (&lt;code&gt;FICLONE&lt;&#x2F;code&gt; on Linux) operation for almost
instant operation. And it won&#x27;t take any additional space.&lt;&#x2F;p&gt;
&lt;div class=&quot;callout&quot;&gt;
&lt;p&gt;&lt;strong&gt;Quick setup checklist:&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Linux with XFS or ZFS, macOS with APFS, or FreeBSD with ZFS&lt;&#x2F;li&gt;
&lt;li&gt;PostgreSQL 18+ cluster on that filesystem&lt;&#x2F;li&gt;
&lt;li&gt;Set &lt;code&gt;file_copy_method = clone&lt;&#x2F;code&gt; in your config&lt;&#x2F;li&gt;
&lt;li&gt;Reload configuration&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;the-benchmark&quot;&gt;The benchmark&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-benchmark&quot; aria-label=&quot;Anchor link for: the-benchmark&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;We need some dummy data to copy. This is the only part of the tutorial where you
have to wait. Let&#x27;s generate a ~6GB database.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE DATABASE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; source_db&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;\c source_db&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; boring_data&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;serial PRIMARY KEY&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    payload &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;text&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- generate 50m rows&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INSERT INTO&lt;&#x2F;span&gt;&lt;span&gt; boring_data (payload)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; md5(random()::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;text&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;||&lt;&#x2F;span&gt;&lt;span&gt; md5(random()::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;text&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; generate_series&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;50000000&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- force a checkpoint&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CHECKPOINT&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You can verify the database now has roughly 6GB of data.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Name              | source_db&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Owner             | postgres&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Encoding          | UTF8&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Locale Provider   | libc&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Collate           | en_US.UTF-8&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Ctype             | en_US.UTF-8&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Locale            |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ICU Rules         |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Access privileges |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Size              | 6289 MB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Tablespace        | pg_default&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Description       |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;While enabling &lt;code&gt;\timing&lt;&#x2F;code&gt; you can test the default (WAL_LOG) strategy. And on my
test volume (relatively slow storage) I get&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;CREATE DATABASE slow_copy TEMPLATE source_db;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;CREATE DATABASE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Time: 67000.615 ms (01:07.001)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now, let&#x27;s verify our configuration is set for speed:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;show file_copy_method;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; file_copy_method&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;------------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; clone&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(1 row)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Let&#x27;s request the semi-instant clone of the same database, without taking
extra disk space at the same time.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;CREATE DATABASE fast_clone TEMPLATE source_db STRATEGY=FILE_COPY;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;CREATE DATABASE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Time: 212.053 ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That&#x27;s a quite an improvement, isn&#x27;t it?&lt;&#x2F;p&gt;
&lt;h2 id=&quot;working-with-cloned-data&quot;&gt;Working with cloned data&lt;a class=&quot;zola-anchor&quot; href=&quot;#working-with-cloned-data&quot; aria-label=&quot;Anchor link for: working-with-cloned-data&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;That was the simple part. But what is happening behind the scenes?&lt;&#x2F;p&gt;
&lt;p&gt;When you clone a database with &lt;code&gt;file_copy_method = clone&lt;&#x2F;code&gt;, PostgreSQL doesn&#x27;t
duplicate any data. The filesystem creates new metadata entries that point to
the same physical &lt;a href=&quot;&#x2F;posts&#x2F;introduction-to-buffers&#x2F;&quot;&gt;8KB pages&lt;&#x2F;a&gt;. Both databases share identical storage.&lt;&#x2F;p&gt;
&lt;p&gt;This can create some initial confusion. If you ask PostgreSQL for the size:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; pg_database_size(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;source_db&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;as&lt;&#x2F;span&gt;&lt;span&gt; source,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       pg_database_size(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;fast_clone&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;as&lt;&#x2F;span&gt;&lt;span&gt; clone;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;PostgreSQL reports both as ~6GB because that&#x27;s the logical size - how much data
each database &quot;contains&quot; - i.e. logical size.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-[ RECORD 1 ]------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;source | 6594041535&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;clone  | 6594041535&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The interesting part happens when you start writing. PostgreSQL doesn&#x27;t update
tuples in place. When you UPDATE a row, it writes a new tuple version somewhere
(often a different page entirely) and marks the old one as dead. The filesystem
doesn&#x27;t care about PostgreSQL internals - it just sees writes to &lt;a href=&quot;&#x2F;posts&#x2F;inside-the-8kb-page&#x2F;&quot;&gt;8KB pages&lt;&#x2F;a&gt;. Any
write to a shared page triggers a copy of that entire page.&lt;&#x2F;p&gt;
&lt;p&gt;A single UPDATE will therefore trigger copy-on-write on multiple pages:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;the page holding the old tuple&lt;&#x2F;li&gt;
&lt;li&gt;the page receiving the new tuple&lt;&#x2F;li&gt;
&lt;li&gt;index pages if any indexed columns changed&lt;&#x2F;li&gt;
&lt;li&gt;FSM and visibility map pages as PostgreSQL tracks free space&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;And later, &lt;a href=&quot;&#x2F;posts&#x2F;vacuum-is-lie&#x2F;&quot;&gt;VACUUM touches even more pages while cleaning up dead tuples&lt;&#x2F;a&gt;. In this
case diverging quickly from the linked storage.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xfs-proof&quot;&gt;XFS proof&lt;a class=&quot;zola-anchor&quot; href=&quot;#xfs-proof&quot; aria-label=&quot;Anchor link for: xfs-proof&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Using the database OID and relfilenode we can verify the both databases are now
sharing physical blocks.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;root@clone-demo:&#x2F;var&#x2F;lib&#x2F;postgresql# sudo filefrag -v &#x2F;var&#x2F;lib&#x2F;postgresql&#x2F;18&#x2F;main&#x2F;base&#x2F;16402&#x2F;16404&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Filesystem type is: 58465342&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;File size of &#x2F;var&#x2F;lib&#x2F;postgresql&#x2F;18&#x2F;main&#x2F;base&#x2F;16402&#x2F;16404 is 1073741824 (262144 blocks of 4096 bytes)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; ext:     logical_offset:        physical_offset: length:   expected: flags:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   0:        0..    2031:   10471550..  10473581:   2032:             shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   1:     2032..   16367:   10474098..  10488433:  14336:   10473582: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   2:    16368..   32751:   10497006..  10513389:  16384:   10488434: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   3:    32752..   65519:   10522066..  10554833:  32768:   10513390: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   4:    65520..  129695:   10571218..  10635393:  64176:   10554834: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   5:   129696..  195231:   10635426..  10700961:  65536:   10635394: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   6:   195232..  262143:   10733730..  10800641:  66912:   10700962: last,shared,eof&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&#x2F;var&#x2F;lib&#x2F;postgresql&#x2F;18&#x2F;main&#x2F;base&#x2F;16402&#x2F;16404: 7 extents found&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;root@clone-demo:&#x2F;var&#x2F;lib&#x2F;postgresql#&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;root@clone-demo:&#x2F;var&#x2F;lib&#x2F;postgresql#&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;root@clone-demo:&#x2F;var&#x2F;lib&#x2F;postgresql# sudo filefrag -v &#x2F;var&#x2F;lib&#x2F;postgresql&#x2F;18&#x2F;main&#x2F;base&#x2F;16418&#x2F;16404&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Filesystem type is: 58465342&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;File size of &#x2F;var&#x2F;lib&#x2F;postgresql&#x2F;18&#x2F;main&#x2F;base&#x2F;16418&#x2F;16404 is 1073741824 (262144 blocks of 4096 bytes)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; ext:     logical_offset:        physical_offset: length:   expected: flags:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   0:        0..    2031:   10471550..  10473581:   2032:             shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   1:     2032..   16367:   10474098..  10488433:  14336:   10473582: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   2:    16368..   32751:   10497006..  10513389:  16384:   10488434: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   3:    32752..   65519:   10522066..  10554833:  32768:   10513390: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   4:    65520..  129695:   10571218..  10635393:  64176:   10554834: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   5:   129696..  195231:   10635426..  10700961:  65536:   10635394: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   6:   195232..  262143:   10733730..  10800641:  66912:   10700962: last,shared,eof&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&#x2F;var&#x2F;lib&#x2F;postgresql&#x2F;18&#x2F;main&#x2F;base&#x2F;16418&#x2F;16404: 7 extents found&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;All it takes is to update some rows using&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;update&lt;&#x2F;span&gt;&lt;span&gt; boring_data &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;set&lt;&#x2F;span&gt;&lt;span&gt; payload &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;new value&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span&gt; id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;where&lt;&#x2F;span&gt;&lt;span&gt; id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;select&lt;&#x2F;span&gt;&lt;span&gt; id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; boring_data &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;limit&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 20&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and the situation will start to change.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;root@clone-demo:&#x2F;var&#x2F;lib&#x2F;postgresql# sudo filefrag -v &#x2F;var&#x2F;lib&#x2F;postgresql&#x2F;18&#x2F;main&#x2F;base&#x2F;16402&#x2F;16404&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Filesystem type is: 58465342&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;File size of &#x2F;var&#x2F;lib&#x2F;postgresql&#x2F;18&#x2F;main&#x2F;base&#x2F;16402&#x2F;16404 is 1073741824 (262144 blocks of 4096 bytes)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; ext:     logical_offset:        physical_offset: length:   expected: flags:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   0:        0..      39:   10471550..  10471589:     40:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   1:       40..    2031:   10471590..  10473581:   1992:             shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   2:     2032..   16367:   10474098..  10488433:  14336:   10473582: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   3:    16368..   32751:   10497006..  10513389:  16384:   10488434: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   4:    32752..   65519:   10522066..  10554833:  32768:   10513390: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   5:    65520..  129695:   10571218..  10635393:  64176:   10554834: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   6:   129696..  195231:   10635426..  10700961:  65536:   10635394: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   7:   195232..  262143:   10733730..  10800641:  66912:   10700962: last,shared,eof&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&#x2F;var&#x2F;lib&#x2F;postgresql&#x2F;18&#x2F;main&#x2F;base&#x2F;16402&#x2F;16404: 7 extents found&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;root@clone-demo:&#x2F;var&#x2F;lib&#x2F;postgresql# sudo filefrag -v &#x2F;var&#x2F;lib&#x2F;postgresql&#x2F;18&#x2F;main&#x2F;base&#x2F;16418&#x2F;16404&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Filesystem type is: 58465342&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;File size of &#x2F;var&#x2F;lib&#x2F;postgresql&#x2F;18&#x2F;main&#x2F;base&#x2F;16418&#x2F;16404 is 1073741824 (262144 blocks of 4096 bytes)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; ext:     logical_offset:        physical_offset: length:   expected: flags:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   0:        0..      39:   10297326..  10297365:     40:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   1:       40..    2031:   10471590..  10473581:   1992:   10297366: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   2:     2032..   16367:   10474098..  10488433:  14336:   10473582: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   3:    16368..   32751:   10497006..  10513389:  16384:   10488434: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   4:    32752..   65519:   10522066..  10554833:  32768:   10513390: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   5:    65520..  129695:   10571218..  10635393:  64176:   10554834: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   6:   129696..  195231:   10635426..  10700961:  65536:   10635394: shared&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   7:   195232..  262143:   10733730..  10800641:  66912:   10700962: last,shared,eof&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&#x2F;var&#x2F;lib&#x2F;postgresql&#x2F;18&#x2F;main&#x2F;base&#x2F;16418&#x2F;16404: 8 extents found&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;root@clone-demo:&#x2F;var&#x2F;lib&#x2F;postgresql#&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In this case extent 0 no longer has shared flag, first 40 blocks size (with
default size 4KB) now diverge, making it total of 160KB. Each database now has
its own copy at different physical address. The remaining extents are still
shared.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;things-to-be-aware-of&quot;&gt;Things to be aware of&lt;a class=&quot;zola-anchor&quot; href=&quot;#things-to-be-aware-of&quot; aria-label=&quot;Anchor link for: things-to-be-aware-of&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Cloning is tempting but there&#x27;s one serious limitation you need to be aware if
you ever attempt to do it in production. The source database can&#x27;t have any
active connections during cloning. This is a PostgreSQL limitation, not a
filesystem one. For production use, this usually means you create a dedicated
template database rather than cloning your live database directly. Or given the
relatively short time the operation takes you have to schedule the cloning in
times where you can temporary block&#x2F;terminate all connections.&lt;&#x2F;p&gt;
&lt;p&gt;Other limitation is that the cloning only works within a single filesystem. If
your databases spans multiple table spaces on different mount points, cloning
will fall back to regular physical copy.&lt;&#x2F;p&gt;
&lt;p&gt;Finally, in most managed cloud environments (AWS RDS, Google Cloud SQL), you
will not have access to the underlying filesystem to configure this. You are
stuck with their proprietary (and often billed) functionality. But for your own
VMs or bare metal? Go ahead and try it.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Custom PostgreSQL extensions with Rust</title>
        <published>2024-05-24T00:00:00+00:00</published>
        <updated>2024-05-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/custom-postgresql-extensions-with-rust/"/>
        <id>https://boringsql.com/posts/custom-postgresql-extensions-with-rust/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/custom-postgresql-extensions-with-rust/">&lt;p&gt;This article explores the pgrx framework, which simplifies the creation of custom PostgreSQL extensions to bring more logic closer to your database. Traditionally, writing such extensions required familiarity with C and a deep understanding of PostgreSQL internals, which could be quite challenging. &lt;code&gt;pgrx&lt;&#x2F;code&gt; lowers the barrier and allows developers to use Rust, known for its safety and performance, making the process of creating efficient and safe database extensions much more accessible.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pg-sysload&quot;&gt;pg_sysload&lt;a class=&quot;zola-anchor&quot; href=&quot;#pg-sysload&quot; aria-label=&quot;Anchor link for: pg-sysload&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;When working with large datasets and migrations (as discussed in &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;boringsql.com&#x2F;posts&#x2F;how-not-to-change-postgresql-column-type&#x2F;&quot;&gt;How Not to Change PostgreSQL Column Type&lt;&#x2F;a&gt;), or during resource-intensive maintenance tasks, you&#x27;ll want to optimise speed and minimise disruption to other processes. One way to control the pace of batch operations is to consider the load on the underlying system.&lt;&#x2F;p&gt;
&lt;p&gt;Many Unix-based systems (we will focus on Linux) provide a valuable metric called the &lt;strong&gt;system load average&lt;&#x2F;strong&gt;. This average consists of three values: the 1-minute, 5-minute, and 15-minute load averages. The load average is not normalised for the number of CPU cores, so a load average of 1 on a single-core system means full utilisation, while on a quad-core system, it indicates 25% utilisation.&lt;&#x2F;p&gt;
&lt;p&gt;In many cases, the system load average is also an excellent indicator of how ongoing operations are impacting a busy database cluster. In this article, we will create a PostgreSQL extension with a function called &lt;code&gt;sys_loadavg()&lt;&#x2F;code&gt; that retrieves this load information. We will use the &lt;code&gt;&#x2F;proc&#x2F;loadavg&lt;&#x2F;code&gt; file (part of the &lt;strong&gt;proc filesystem&lt;&#x2F;strong&gt;), which exposes underlying system details.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;getting-started-with-pgrx&quot;&gt;Getting Started with &lt;code&gt;pgrx&lt;&#x2F;code&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#getting-started-with-pgrx&quot; aria-label=&quot;Anchor link for: getting-started-with-pgrx&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Before we start, ensure you have:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Rust installed (&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.rust-lang.org&#x2F;learn&#x2F;get-started&quot;&gt;Get Started with Rust&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pgcentralfoundation&#x2F;pgrx?tab=readme-ov-file#system-requirements&quot;&gt;System dependencies&lt;&#x2F;a&gt; installed for &lt;code&gt;pgrx&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;With these prerequisites in place, you can install &lt;code&gt;pgrx&lt;&#x2F;code&gt; itself and create a new extension skeleton:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;cargo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; install&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --locked&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; cargo-pgrx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;cargo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pgrx new pg_sysload&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;cd&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pg_sysload&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This gives you a complete environment for developing your own PostgreSQL extensions in Rust. While Rust might seem daunting at first (especially with its async and other features), the language itself is quite powerful. Its ease of extension creation makes it worth a second look.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;setting-up-the-extension&quot;&gt;Setting Up the Extension&lt;a class=&quot;zola-anchor&quot; href=&quot;#setting-up-the-extension&quot; aria-label=&quot;Anchor link for: setting-up-the-extension&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;While the &lt;code&gt;pgrx new&lt;&#x2F;code&gt; command sets up a basic template, we will create a more sophisticated extension. Its primary logic involves parsing the &lt;code&gt;&#x2F;proc&#x2F;loadavg&lt;&#x2F;code&gt; file, extracting its values, and returning them to the user.&lt;&#x2F;p&gt;
&lt;p&gt;The logic itself is straightforward:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;#[pg_extern]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;fn&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; sys_loadavg&lt;&#x2F;span&gt;&lt;span&gt;()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; -&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; Option&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Vec&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;f64&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    &#x2F;&#x2F; Read the contents of the &#x2F;proc&#x2F;loadavg&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    let mut&lt;&#x2F;span&gt;&lt;span&gt; file&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = match&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; fs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;File&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;open&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&#x2F;proc&#x2F;loadavg&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;        Ok&lt;&#x2F;span&gt;&lt;span&gt;(file)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; file,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;        Err&lt;&#x2F;span&gt;&lt;span&gt;(err)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;            pgrx&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;error!&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;Error reading &#x2F;proc&#x2F;loadavg: {}&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, err);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    let mut&lt;&#x2F;span&gt;&lt;span&gt; contents&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; String&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;new&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    if let&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; Err&lt;&#x2F;span&gt;&lt;span&gt;(err)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; file&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;read_to_string&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;amp;mut&lt;&#x2F;span&gt;&lt;span&gt; contents) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;        pgrx&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;error!&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;Error reading &#x2F;proc&#x2F;loadavg: {}&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, err);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    &#x2F;&#x2F; Extract the load average fields&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    let&lt;&#x2F;span&gt;&lt;span&gt; fields&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; contents&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;split_whitespace&lt;&#x2F;span&gt;&lt;span&gt;()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;collect&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Vec&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;_&amp;gt;&amp;gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    if&lt;&#x2F;span&gt;&lt;span&gt; fields&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;len&lt;&#x2F;span&gt;&lt;span&gt;()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 3&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;        Some&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            fields[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;..&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;3&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;                .&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;iter&lt;&#x2F;span&gt;&lt;span&gt;()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;                .&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;filter_map&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;|&lt;&#x2F;span&gt;&lt;span&gt;s&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;|&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; f64&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;from_str&lt;&#x2F;span&gt;&lt;span&gt;(s)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ok&lt;&#x2F;span&gt;&lt;span&gt;())&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;                .&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;collect&lt;&#x2F;span&gt;&lt;span&gt;(),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        )&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; else&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;        pgrx&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;error!&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;Invalid format in &#x2F;proc&#x2F;loadavg&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;To limit usage to systems supporting the &lt;strong&gt;proc filesystem&lt;&#x2F;strong&gt;, we check for the presence of the file when the extension is loaded:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;#[pg_guard]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;fn&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; _PG_init&lt;&#x2F;span&gt;&lt;span&gt;() {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    INIT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;call_once&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;||&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        let&lt;&#x2F;span&gt;&lt;span&gt; loadavg_available&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; fs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;metadata&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&#x2F;proc&#x2F;loadavg&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;is_ok&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        if !&lt;&#x2F;span&gt;&lt;span&gt;loadavg_available {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;            pgrx&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;error!&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&#x2F;proc&#x2F;loadavg not found. Extension cannot load.&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    });&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This basic check prevents users from loading the extension on incompatible systems. With minor details omitted, that&#x27;s almost all there is to it.&lt;&#x2F;p&gt;
&lt;p&gt;You can find the complete &lt;code&gt;lib.rs&lt;&#x2F;code&gt; file in the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;boringSQL&#x2F;pg_sysload&quot;&gt;accompanying GitHub repository&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;running-the-extension&quot;&gt;Running the Extension&lt;a class=&quot;zola-anchor&quot; href=&quot;#running-the-extension&quot; aria-label=&quot;Anchor link for: running-the-extension&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Where &lt;code&gt;pgrx&lt;&#x2F;code&gt; truly shines is in all the heavy lifting it does for you. It leverages the &lt;code&gt;cargo&lt;&#x2F;code&gt; command. To initialise the extension, run:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;cargo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pgrx init&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This downloads and prepares PostgreSQL source code for versions 12 to 16 (at the time of writing). After a bit of waiting (hopefully successfully, if you have all the system dependencies), run the extension:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;cargo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pgrx run&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You&#x27;ll get a &lt;code&gt;psql&lt;&#x2F;code&gt; prompt for a dedicated PostgreSQL instance. Create and use the extension:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE&lt;&#x2F;span&gt;&lt;span&gt; EXTENSION pg_sysload;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now you can try the newly exposed function:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; sys_loadavg();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   sys_loadavg&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;------------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;17&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;57&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;32&lt;&#x2F;span&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; row&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And that&#x27;s all. Your very first PostgreSQL extension is working.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;throttling-batch-processing&quot;&gt;Throttling Batch Processing&lt;a class=&quot;zola-anchor&quot; href=&quot;#throttling-batch-processing&quot; aria-label=&quot;Anchor link for: throttling-batch-processing&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;With the extension ready, let&#x27;s revisit our original goal: throttling long-running batch data processing. The data from &lt;code&gt;sys_loadavg&lt;&#x2F;code&gt; can be used in various ways:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dynamic Sleep Times:&lt;&#x2F;strong&gt; Insert calculated sleep intervals based on the system load.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Dynamic Batch Sizes:&lt;&#x2F;strong&gt; Adjust the number of rows processed per batch based on available resources.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Here&#x27;s an example:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Sleep for 5 seconds multiplied by the 1-minute load of the system&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; pg_sleep((sys_loadavg())[1]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 5&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Assume 20 cores and for each one available, add 100 rows to process&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; ... &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LIMIT&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;20&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; -&lt;&#x2F;span&gt;&lt;span&gt; (sys_loadavg())[1])::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;int *&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 100&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This allows you to fine-tune processing for unsupervised operation, automatically adapting to the current system load.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;a class=&quot;zola-anchor&quot; href=&quot;#conclusion&quot; aria-label=&quot;Anchor link for: conclusion&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;And there you have it! You&#x27;ve just built your first PostgreSQL extension using Rust and the &lt;code&gt;pgrx&lt;&#x2F;code&gt; framework. I have always found the prospect of writing an extension quite daunting (probably because I&#x27;m long gone from the C-ecosystem), but this wasn&#x27;t so bad. You&#x27;ve now got a handy tool for monitoring system load, perfect for keeping tabs on how your database processes long-running migrations.&lt;&#x2F;p&gt;
&lt;p&gt;This is just scratching the surface. We haven&#x27;t even touched on the really fun stuff – extending PostgreSQL&#x27;s internals with custom data types, operators, or indexes. You could build extensions that transform or aggregate data, hook into external APIs, or create background workers, bringing various business logic directly into the database engine. And yes, tapping into the database&#x27;s core can be a bit risky, and it is always important to assess risks and recovery options. But whatever you choose to create, remember, with &lt;code&gt;pgrx&lt;&#x2F;code&gt; at your side, you&#x27;ve got the power of Rust to keep things safe and sound.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The Bloat Busters: pg_repack vs pg_squeeze</title>
        <published>2024-04-27T00:00:00+00:00</published>
        <updated>2024-04-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/the-bloat-busters-pg-repack-pg-squeeze/"/>
        <id>https://boringsql.com/posts/the-bloat-busters-pg-repack-pg-squeeze/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/the-bloat-busters-pg-repack-pg-squeeze/">&lt;p&gt;As the database size increases and the number of transactions per second rise, you&#x27;ll inevitably face the challenge of the table bloat. Although PostgreSQL assists as much as possible with its &lt;a href=&quot;&#x2F;posts&#x2F;vacuum-is-lie&#x2F;&quot;&gt;auto-vacuum feature&lt;&#x2F;a&gt;, there will come a time when you will compel whether to run &lt;code&gt;VACUUM FULL&lt;&#x2F;code&gt;. Unless you have option of longish downtime windows, this is not an easy decision.&lt;&#x2F;p&gt;
&lt;p&gt;Thankfully, the rich ecosystem of PostgreSQL offers more than one solution how to make it simpler. Ignoring older tools like &lt;code&gt;pg_reorg&lt;&#x2F;code&gt;, two contenders worth considering are &lt;strong&gt;pg_repack&lt;&#x2F;strong&gt; and &lt;strong&gt;pg_squeeze&lt;&#x2F;strong&gt;. This article dives into their strengths and weaknesses to help you decide which one is better for your specific use case.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cases-of-heavy-duty-maintenance&quot;&gt;Cases of Heavy Duty Maintenance&lt;a class=&quot;zola-anchor&quot; href=&quot;#cases-of-heavy-duty-maintenance&quot; aria-label=&quot;Anchor link for: cases-of-heavy-duty-maintenance&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;In scenarios with a continuous and predictable transaction pattern, you can usually rely on auto-vacuum. However, there are use cases where this won&#x27;t be sufficient. Such examples typically involve &quot;bulk&quot; operations—whether it&#x27;s bulk imports, deletions, or a combination of both. Imagine scenarios where:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;You &lt;a href=&quot;&#x2F;posts&#x2F;how-not-to-change-postgresql-column-type&#x2F;&quot;&gt;migrate one or more column data types&lt;&#x2F;a&gt; over a longer period (and no, using &lt;code&gt;ALTER COLUMN name TYPE new_type&lt;&#x2F;code&gt; is not the best option).&lt;&#x2F;li&gt;
&lt;li&gt;You drop a column that has been moved to a different table.&lt;&#x2F;li&gt;
&lt;li&gt;You need to modify a large amount of data, using soft-deletes and later &lt;a href=&quot;&#x2F;posts&#x2F;deletes-are-difficult&#x2F;&quot;&gt;actual DELETEs&lt;&#x2F;a&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;Due to changes in compliance requirements, you need to DELETE a massive amount of data.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;In all these cases, you might end up with a huge table and the traditional solution would be &lt;code&gt;VACUUM FULL&lt;&#x2F;code&gt;, which is associated with significant downtime due to table locking.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;alternatives-to-vacuum-full&quot;&gt;Alternatives to VACUUM FULL&lt;a class=&quot;zola-anchor&quot; href=&quot;#alternatives-to-vacuum-full&quot; aria-label=&quot;Anchor link for: alternatives-to-vacuum-full&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Due to the limitations of &lt;code&gt;VACUUM FULL&lt;&#x2F;code&gt;, several alternatives have emerged. The first contender in this space was &lt;code&gt;pg_reorg&lt;&#x2F;code&gt; (which this comparison will not cover), later superseded by &lt;code&gt;pg_repack&lt;&#x2F;code&gt;. The relatively new kid on the block is &lt;code&gt;pg_squeeze&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Each solution comes with different architectures, deployment methods, and sets of pros and cons.&lt;&#x2F;p&gt;
&lt;p&gt;As an added benefit, both tools can effectively serve as alternatives to &lt;code&gt;CLUSTER&lt;&#x2F;code&gt; (which also requires the exclusive table lock similar to full vacuum), making them effective in optimising data storage based on the given order.&lt;&#x2F;p&gt;
&lt;p&gt;Both tools also share common behaviour. They won’t magically remove the bloat—you need to have at least the same amount of space available as the new table will require. Therefore, deploying both tools needs to be considered well before you reach critical levels of available disk space. Both will also generate a significant volume of WAL files — affecting &lt;a href=&quot;&#x2F;posts&#x2F;inside-the-8kb-page&#x2F;&quot;&gt;every 8KB page&lt;&#x2F;a&gt; along the way — which need to be stored, processed, backed up, etc.&lt;&#x2F;p&gt;
&lt;p&gt;While not directly comparable to these tools, another method that can be used manually is table partitioning. Although it won’t be included in this comparison, if planned in advance, it can simplify certain maintenance tasks, improve performance, and make routine vacuuming faster and more efficient. It&#x27;s only fair to say - it all depends on the specific scenarios and usage.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pg-repack&quot;&gt;pg_repack&lt;a class=&quot;zola-anchor&quot; href=&quot;#pg-repack&quot; aria-label=&quot;Anchor link for: pg-repack&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;At the time of writing this article, &lt;code&gt;pg_repack&lt;&#x2F;code&gt; can be considered the most well-known solution for combating table bloat in the PostgreSQL ecosystem. Built as an extension, it&#x27;s easy to install (either from package repositories or via a self-compiled artifact) and its setup does not require a cluster restart.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;pg_repack&lt;&#x2F;code&gt; works by creating a new copy of the table being processed, setting up triggers to replicate new data while it fills up with the existing data. An exclusive full table lock is required both at the start and finish of the process when swapping the old and new tables.&lt;&#x2F;p&gt;
&lt;p&gt;The maintenance is initiated from the CLI and allows you to specify a number of arguments to fine-tune the repacking of individual tables to match the requirements. &lt;code&gt;pg_repack&lt;&#x2F;code&gt; allows re-clustering of data based on columns only, i.e., it does not explicitly require an index and can therefore overcome some limitations of &lt;code&gt;pg_squeeze&lt;&#x2F;code&gt; in this regard.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;pg_repack&lt;&#x2F;code&gt; is very effective in reclaiming space and can work with all types of bloat. It&#x27;s available out of the box both on Amazon RDS and Google Cloud SQL.&lt;&#x2F;p&gt;
&lt;p&gt;The drawback you might experience when terminating the process (which may be necessary for various reasons, such as impact on the running environment) is that it won’t clean up all the fragments as it won&#x27;t remain connected to the target database.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pg-squeeze&quot;&gt;pg_squeeze&lt;a class=&quot;zola-anchor&quot; href=&quot;#pg-squeeze&quot; aria-label=&quot;Anchor link for: pg-squeeze&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Compared to the previous solution, &lt;code&gt;pg_squeeze&lt;&#x2F;code&gt; is built differently, relying on logical decoding instead of triggers. Its main benefit is a lower impact on the host system during table rebuilding, improving availability and stability.&lt;&#x2F;p&gt;
&lt;p&gt;Like &lt;code&gt;pg_repack&lt;&#x2F;code&gt;, &lt;code&gt;pg_squeeze&lt;&#x2F;code&gt; creates a new table and copies the existing data from the bloated table. Logical replication is involved in streaming changes from the original table to the newly created one in real-time. This allows the new table to stay up-to-date during the process without unnecessary impact on the regular operations performed on the bloated table. Thus, &lt;code&gt;pg_squeeze&lt;&#x2F;code&gt; significantly reduces the need for locking. The exclusive lock is needed only during the final phase of the operation, when the old table is swapped out for the new, optimized table. The duration of the exclusive lock can also be configured.&lt;&#x2F;p&gt;
&lt;p&gt;While &lt;code&gt;pg_squeeze&lt;&#x2F;code&gt; is also available as an extension, its deployment necessitates configuration changes involving &lt;code&gt;wal_level&lt;&#x2F;code&gt;, &lt;code&gt;max_replication_slots&lt;&#x2F;code&gt;, and &lt;code&gt;shared_preload_libraries&lt;&#x2F;code&gt; — the same settings used for &lt;a href=&quot;&#x2F;posts&#x2F;logical-replication-beyond-the-basics&#x2F;&quot;&gt;logical replication&lt;&#x2F;a&gt;. Due to this, a restart of the cluster is required.&lt;&#x2F;p&gt;
&lt;p&gt;On the other hand, &lt;code&gt;pg_squeeze&lt;&#x2F;code&gt; is designed for regular, rather than ad-hoc processing only. You can register a table for regular processing, and whenever the table meets the criteria to be &quot;squeezed,&quot; a task will be added to a queue, where it will be sequentially processed in the order they were created. The automated processing offers basic options usable in most scenarios, but you might find it limited if you need to work around other operational constraints of the cluster&#x2F;environment. Having said that, the maintenance of the table can also be triggered manually.&lt;&#x2F;p&gt;
&lt;p&gt;While &lt;code&gt;pg_squeeze&lt;&#x2F;code&gt; might be considered superior to &lt;code&gt;pg_repack&lt;&#x2F;code&gt; in terms of maintenance operations and impact, it comes with a significant caveat when reclaiming space—compared to &lt;code&gt;pg_repack&lt;&#x2F;code&gt;, it copies the full rows as they are. This behaviour renders it ineffective at removing bloat created due to dropped columns (behaviour still present at the time of the writing of this article at the end of April 2024).&lt;&#x2F;p&gt;
&lt;p&gt;As already mentioned, &lt;code&gt;pg_squeeze&lt;&#x2F;code&gt; can use a specified index for clustering when needed. The limitation you might find is that clustering cannot be performed on a partial index. Compared to &lt;code&gt;pg_repack&lt;&#x2F;code&gt;, it always seems to clean up all the artefacts accordingly (thanks to the always-running worker process).&lt;&#x2F;p&gt;
&lt;p&gt;Unfortunately at the moment of writing the article &lt;code&gt;pg_squeeze&lt;&#x2F;code&gt; is not available for Amazon RDS, only Google Cloud SQL.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;a class=&quot;zola-anchor&quot; href=&quot;#conclusion&quot; aria-label=&quot;Anchor link for: conclusion&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;It&#x27;s remarkable to have two mature and production-tested tools at your disposal. Deciding between them comes down to the specific requirements, the use cases, and the operational specifics of the business services.&lt;&#x2F;p&gt;
&lt;p&gt;The very opinionated difference between the tools can be made, using &lt;code&gt;pg_squeeze&lt;&#x2F;code&gt; for automated, continuous cleaning of specific tables, and &lt;code&gt;pg_repack&lt;&#x2F;code&gt; as the heavyweight champion of controlled setups.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;(Edit) Added availability on Amazon RDS and Google Cloud SQL.&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
