<?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</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/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://boringsql.com"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-03-29T14:47:00+02:00</updated>
    <id>https://boringsql.com/atom.xml</id>
    <entry xml:lang="en">
        <title>Good CTE, bad CTE</title>
        <published>2026-03-29T14:47:00+02:00</published>
        <updated>2026-03-29T14:47:00+02:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/good-cte-bad-cte/"/>
        <id>https://boringsql.com/posts/good-cte-bad-cte/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/good-cte-bad-cte/">&lt;p&gt;The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;queries-with.html&quot;&gt;Common Table Expression&lt;&#x2F;a&gt;, or CTE, is often the first feature developers reach for beyond basic SQL, and often the only one. You write a subquery after &lt;code&gt;WITH&lt;&#x2F;code&gt;, give it a name, and use it in the rest of your query. It only exists for the duration of that query.&lt;&#x2F;p&gt;
&lt;p&gt;But the popularity of CTEs usually has less to do with modernizing code and more to do with the promise of imperative logic. For many, CTE acts as an easy to understand remedy for &#x27;scary queries&#x27; and way how to force execution order on the database. The way how many write queries is as if they tell optimizer &quot;first do this, then do that&quot;.&lt;&#x2F;p&gt;
&lt;p&gt;This creates a problem. CTEs handle query decomposition, recursion and multi statement DDLs. Planner treats them differently depending how you write and use them though. For long time (prior PostgreSQL 12) CTEs acted as optimization fence. The planner couldn&#x27;t push predicates into them, couldn&#x27;t use indexes on the underlying tables. Couldn&#x27;t do anything that materialize them and scan through the result.&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL 12 changed this. CTEs now get inlined, materialized, or something in between, depending on how you write them.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sample-schema&quot;&gt;Sample schema&lt;a class=&quot;zola-anchor&quot; href=&quot;#sample-schema&quot; aria-label=&quot;Anchor link for: sample-schema&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;We will use the same schema as in the article &lt;a href=&quot;&#x2F;posts&#x2F;postgresql-statistics&#x2F;&quot;&gt;PostgreSQL Statistics: Why queries run slow&lt;&#x2F;a&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;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; customers&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 style=&quot;color: #F97583;&quot;&gt;    name text NOT NULL&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;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; 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 REFERENCES&lt;&#x2F;span&gt;&lt;span&gt; customers(id),&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;    note &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&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;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; orders_archive&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LIKE&lt;&#x2F;span&gt;&lt;span&gt; orders INCLUDING ALL EXCLUDING &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IDENTITY&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;INSERT INTO&lt;&#x2F;span&gt;&lt;span&gt; customers (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;name&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;SELECT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Customer &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span&gt; i&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;2000&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; i;&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; orders (customer_id, amount, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;status&lt;&#x2F;span&gt;&lt;span&gt;, note, 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; 1999&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; 500&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: #F97583;&quot;&gt;    CASE WHEN&lt;&#x2F;span&gt;&lt;span&gt; random()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;lt;&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;3&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; THEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Some note text here for padding&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ELSE NULL END&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;2022-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; 1095&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;100000&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&gt;ANALYZE customers;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ANALYZE orders;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For recursive examples later on we&#x27;ll also need an &lt;code&gt;employees&lt;&#x2F;code&gt; table with a self-referencing hierarchy:&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; employees&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 style=&quot;color: #F97583;&quot;&gt;    name text 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;    manager_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;integer REFERENCES&lt;&#x2F;span&gt;&lt;span&gt; employees(id),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    department &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;text NOT NULL&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; employees (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span&gt;, manager_id, department) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&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: #9ECBFF;&quot;&gt;&amp;#39;Alice&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,   &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;NULL&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Engineering&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;    (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Bob&amp;#39;&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: #9ECBFF;&quot;&gt;&amp;#39;Engineering&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;    (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Charlie&amp;#39;&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: #9ECBFF;&quot;&gt;&amp;#39;Engineering&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;    (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Diana&amp;#39;&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: #9ECBFF;&quot;&gt;&amp;#39;Engineering&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;    (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Eve&amp;#39;&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: #9ECBFF;&quot;&gt;&amp;#39;Engineering&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;    (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Frank&amp;#39;&lt;&#x2F;span&gt;&lt;span&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;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Sales&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;    (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Grace&amp;#39;&lt;&#x2F;span&gt;&lt;span&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;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Sales&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;    (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Hank&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,    &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;6&lt;&#x2F;span&gt;&lt;span&gt;,    &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Sales&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;    (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Ivy&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,     &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;6&lt;&#x2F;span&gt;&lt;span&gt;,    &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Sales&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ANALYZE employees;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;the-optimization-fence-era-pre-pg-12&quot;&gt;The optimization fence era (pre-PG 12)&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-optimization-fence-era-pre-pg-12&quot; aria-label=&quot;Anchor link for: the-optimization-fence-era-pre-pg-12&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;As we already covered before PostgreSQL 12, every CTE was materialized. No exceptions. The planner would compute the CTE result set in full, store it in a temporary tuplestore, and then scan that tuplestore whenever the main query referenced the CTE. This made CTEs an &lt;strong&gt;optimization fence&lt;&#x2F;strong&gt; because the planner could not look through them.&lt;&#x2F;p&gt;
&lt;p&gt;Consider this simple query:&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;WITH&lt;&#x2F;span&gt;&lt;span&gt; filtered &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; 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;2025-01-01&amp;#39;&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;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; filtered &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;p&gt;On PostgreSQL 11 and earlier, the &lt;code&gt;EXPLAIN&lt;&#x2F;code&gt; output would look something 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;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; CTE Scan on filtered  (cost=1840.00..2290.00 rows=2 width=58)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Filter: (status = &amp;#39;pending&amp;#39;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   CTE filtered&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     -&amp;gt;  Seq Scan on orders  (cost=0.00..1840.00 rows=10000 width=58)&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;2025-01-01&amp;#39;::date)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Notice what happens here. The CTE runs a sequential scan on &lt;code&gt;orders&lt;&#x2F;code&gt; with the date filter. It materializes all matching rows. Then the outer query applies the &lt;code&gt;status = &#x27;pending&#x27;&lt;&#x2F;code&gt; filter &lt;em&gt;after&lt;&#x2F;em&gt; materialization. Even if a composite index on &lt;code&gt;(created_at, status)&lt;&#x2F;code&gt; existed, the planner couldn&#x27;t use it as it can&#x27;t see through the CTE boundary to combine the predicates.&lt;&#x2F;p&gt;
&lt;p&gt;Why was it designed this way? Two reasons. First, reason was snapshot isolation. Materializing the CTE guaranteed that the result set was computed once, from a single snapshot, regardless of how many times it was referenced. Second, as protection for side-effect edge cases. If a CTE contained a data-modifying statement (&lt;code&gt;INSERT&lt;&#x2F;code&gt;, &lt;code&gt;UPDATE&lt;&#x2F;code&gt;, &lt;code&gt;DELETE&lt;&#x2F;code&gt;), materialising ensured it executed exactly once.&lt;&#x2F;p&gt;
&lt;div class=&quot;sidenote&quot;&gt;The workaround was well-known in the community: rewrite CTEs as subqueries. Subqueries had always been subject to the planner&#x27;s normal optimisation rules, including predicate pushdown and inlining. The same query written as &lt;code&gt;SELECT * FROM (SELECT * FROM orders WHERE created_at &gt; &#x27;2025-01-01&#x27;) sub WHERE status = &#x27;pending&#x27;&lt;&#x2F;code&gt; would produce a much better plan.&lt;&#x2F;div&gt;
&lt;p&gt;This led to an entire workaround culture. Developers would write queries with CTEs for readability during development, then rewrite them as nested subqueries for production. The community had a saying: &lt;em&gt;CTEs are optimization fences&lt;&#x2F;em&gt;. It was repeated so often. Many developers still believe it today. But it hasn&#x27;t been true since PostgreSQL 12.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;postgresql-12-cte-inlining&quot;&gt;PostgreSQL 12: CTE inlining&lt;a class=&quot;zola-anchor&quot; href=&quot;#postgresql-12-cte-inlining&quot; aria-label=&quot;Anchor link for: postgresql-12-cte-inlining&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;PostgreSQL 12 introduced automatic CTE inlining. Non-recursive, side-effect-free, singly-referenced CTEs are now inlined by default. The planner started treating them as subqueries and applies all its normal optimisations. Predicate pushdown, index usage, join reordering apply exactly as if the CTE syntax never existed.&lt;&#x2F;p&gt;
&lt;p&gt;The same query from the previous section now produces a completely different plan:&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;WITH&lt;&#x2F;span&gt;&lt;span&gt; filtered &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; 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;2025-01-01&amp;#39;&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;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; filtered &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; Seq Scan on orders  (cost=0.00..2355.00 rows=2 width=58)&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;2025-01-01&amp;#39;::date) AND (status = &amp;#39;pending&amp;#39;::text))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The CTE is gone from the plan entirely. Both predicates are merged into a single scan on &lt;code&gt;orders&lt;&#x2F;code&gt;. If there&#x27;s a suitable index, the planner can use it. The CTE syntax doesn&#x27;t change the execution plan.&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL 12 also introduced two new keywords that let you override the planner&#x27;s decision:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MATERIALIZED&lt;&#x2F;code&gt; - forces the CTE to materialize, even when the planner would inline it&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;NOT MATERIALIZED&lt;&#x2F;code&gt; - forces inlining, even when the planner would materialize it&lt;&#x2F;li&gt;
&lt;&#x2F;ul&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;-- force materialization&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;WITH&lt;&#x2F;span&gt;&lt;span&gt; filtered &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; MATERIALIZED (&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; 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;2025-01-01&amp;#39;&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;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; filtered &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;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 inlining&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;WITH&lt;&#x2F;span&gt;&lt;span&gt; filtered &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS NOT&lt;&#x2F;span&gt;&lt;span&gt; MATERIALIZED (&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; 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;2025-01-01&amp;#39;&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;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; filtered &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;p&gt;This follows the same principle as &lt;a href=&quot;&#x2F;posts&#x2F;view-inlining&#x2F;&quot;&gt;VIEW inlining&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;when-does-a-cte-get-materialized&quot;&gt;When does a CTE get materialized?&lt;a class=&quot;zola-anchor&quot; href=&quot;#when-does-a-cte-get-materialized&quot; aria-label=&quot;Anchor link for: when-does-a-cte-get-materialized&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;case-1-single-reference-no-side-effects-inlined&quot;&gt;Case 1: single reference, no side effects (INLINED)&lt;a class=&quot;zola-anchor&quot; href=&quot;#case-1-single-reference-no-side-effects-inlined&quot; aria-label=&quot;Anchor link for: case-1-single-reference-no-side-effects-inlined&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;The simplest and most common case. If you reference the CTE exactly once and it contains no side effects, the planner inlines it.&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;WITH&lt;&#x2F;span&gt;&lt;span&gt; recent &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; 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;2025-01-01&amp;#39;&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;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; recent &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; Seq Scan on orders  (cost=0.00..2355.00 rows=2 width=59)&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;2025-01-01&amp;#39;::date) AND (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;(2 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Both predicates are merged. The planner considers all access paths on &lt;code&gt;orders&lt;&#x2F;code&gt; directly.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;case-2-multiple-references-materialized&quot;&gt;Case 2: multiple references (MATERIALIZED)&lt;a class=&quot;zola-anchor&quot; href=&quot;#case-2-multiple-references-materialized&quot; aria-label=&quot;Anchor link for: case-2-multiple-references-materialized&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;When a CTE is referenced more than once, the planner materializes it. This is actually a feature, CTE is computed once and reused. Therefore avoiding redundant work.&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;WITH&lt;&#x2F;span&gt;&lt;span&gt; summary &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    SELECT status&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;count&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;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; cnt &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GROUP BY status&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;SELECT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; a&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;status&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;b&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;status&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; summary a, summary b&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; a&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;cnt&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; b&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;cnt&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; Nested Loop  (cost=2355.04..2355.52 rows=5 width=64)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Join Filter: (a.cnt &amp;gt; b.cnt)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   CTE summary&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     -&amp;gt;  HashAggregate  (cost=2355.00..2355.04 rows=4 width=17)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           Group Key: orders.status&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           -&amp;gt;  Seq Scan on orders  (cost=0.00..1855.00 rows=100000 width=9)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   -&amp;gt;  CTE Scan on summary a  (cost=0.00..0.08 rows=4 width=40)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   -&amp;gt;  CTE Scan on summary b  (cost=0.00..0.08 rows=4 width=40)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(8 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The &lt;code&gt;CTE Scan&lt;&#x2F;code&gt; nodes appear twice, but the &lt;code&gt;HashAggregate&lt;&#x2F;code&gt; runs only once. For expensive computations referenced multiple times, this is exactly what you want.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;case-3-recursive-cte-always-materialized&quot;&gt;Case 3: recursive CTE (ALWAYS MATERIALIZED)&lt;a class=&quot;zola-anchor&quot; href=&quot;#case-3-recursive-cte-always-materialized&quot; aria-label=&quot;Anchor link for: case-3-recursive-cte-always-materialized&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Recursive CTEs must maintain a working table between iterations. There&#x27;s no way to inline them. We&#x27;ll cover recursion in detail later in the article.&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;WITH RECURSIVE&lt;&#x2F;span&gt;&lt;span&gt; subordinates &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    SELECT&lt;&#x2F;span&gt;&lt;span&gt; id, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span&gt;, manager_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; employees &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;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    UNION ALL&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 style=&quot;color: #79B8FF;&quot;&gt; e&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;manager_id&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; employees e&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    JOIN&lt;&#x2F;span&gt;&lt;span&gt; subordinates s &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; e&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;manager_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; s&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&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;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; subordinates;&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; CTE Scan on subordinates  (cost=17.21..18.83 rows=81 width=40)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   CTE subordinates&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     -&amp;gt;  Recursive Union  (cost=0.00..17.21 rows=81 width=13)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           -&amp;gt;  Seq Scan on employees  (cost=0.00..1.11 rows=1 width=13)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                 Filter: (id = 1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           -&amp;gt;  Hash Join  (cost=0.33..1.53 rows=8 width=13)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                 Hash Cond: (e.manager_id = s.id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                 -&amp;gt;  Seq Scan on employees e  (cost=0.00..1.09 rows=9 width=13)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                 -&amp;gt;  Hash  (cost=0.20..0.20 rows=10 width=4)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                       -&amp;gt;  WorkTable Scan on subordinates s  (cost=0.00..0.20 rows=10 width=4)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(10 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;case-4-data-modifying-cte-always-materialized&quot;&gt;Case 4: data-modifying CTE (ALWAYS MATERIALIZED)&lt;a class=&quot;zola-anchor&quot; href=&quot;#case-4-data-modifying-cte-always-materialized&quot; aria-label=&quot;Anchor link for: case-4-data-modifying-cte-always-materialized&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;CTEs that contain &lt;code&gt;INSERT&lt;&#x2F;code&gt;, &lt;code&gt;UPDATE&lt;&#x2F;code&gt;, or &lt;code&gt;DELETE&lt;&#x2F;code&gt; are always materialized. The side effects must execute exactly once, in a predictable order.&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;WITH&lt;&#x2F;span&gt;&lt;span&gt; deleted &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    DELETE FROM&lt;&#x2F;span&gt;&lt;span&gt; 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;cancelled&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt; RETURNING &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&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;&#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 style=&quot;color: #79B8FF;&quot;&gt; count&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;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; deleted;&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; Aggregate  (cost=2670.13..2670.14 rows=1 width=8)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   CTE deleted&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     -&amp;gt;  Delete on orders  (cost=0.00..2105.00 rows=25117 width=6)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           -&amp;gt;  Seq Scan on orders  (cost=0.00..2105.00 rows=25117 width=6)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                 Filter: (status = &amp;#39;cancelled&amp;#39;::text)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   -&amp;gt;  CTE Scan on deleted  (cost=0.00..502.34 rows=25117 width=0)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(6 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The &lt;code&gt;CTE Scan&lt;&#x2F;code&gt; is present because the &lt;code&gt;DELETE&lt;&#x2F;code&gt; must be fully executed before the &lt;code&gt;count(*)&lt;&#x2F;code&gt; can run.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;case-5-volatile-function-materialized&quot;&gt;Case 5: VOLATILE function (MATERIALIZED)&lt;a class=&quot;zola-anchor&quot; href=&quot;#case-5-volatile-function-materialized&quot; aria-label=&quot;Anchor link for: case-5-volatile-function-materialized&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;If a CTE contains a &lt;code&gt;VOLATILE&lt;&#x2F;code&gt; function, the planner materializes it to prevent the function from being evaluated multiple times with potentially different results.&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;WITH&lt;&#x2F;span&gt;&lt;span&gt; rand &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    SELECT&lt;&#x2F;span&gt;&lt;span&gt; id, random()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS&lt;&#x2F;span&gt;&lt;span&gt; r &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; orders&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;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; rand &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; r &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;lt;&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;01&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; CTE Scan on rand  (cost=2105.00..4355.00 rows=33333 width=12)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Filter: (r &amp;lt; &amp;#39;0.01&amp;#39;::double precision)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   CTE rand&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     -&amp;gt;  Seq Scan on orders  (cost=0.00..2105.00 rows=100000 width=12)&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;Even though &lt;code&gt;rand&lt;&#x2F;code&gt; is referenced only once, the &lt;code&gt;CTE Scan&lt;&#x2F;code&gt; is there. &lt;code&gt;random()&lt;&#x2F;code&gt; is &lt;code&gt;VOLATILE&lt;&#x2F;code&gt;, which forces materialization.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;case-6-stable-functions-inlined&quot;&gt;Case 6: STABLE functions (INLINED)&lt;a class=&quot;zola-anchor&quot; href=&quot;#case-6-stable-functions-inlined&quot; aria-label=&quot;Anchor link for: case-6-stable-functions-inlined&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;STABLE&lt;&#x2F;code&gt; functions like &lt;code&gt;now()&lt;&#x2F;code&gt; do &lt;strong&gt;not&lt;&#x2F;strong&gt; prevent inlining. The reason is that the time is frozen at transaction start.&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;WITH&lt;&#x2F;span&gt;&lt;span&gt; recent &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; orders&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&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; now&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; interval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;7 days&amp;#39;&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;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; recent &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; Seq Scan on orders  (cost=0.00..2855.00 rows=2 width=59)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Filter: ((status = &amp;#39;pending&amp;#39;::text) AND (created_at &amp;gt; (now() - &amp;#39;7 days&amp;#39;::interval)))&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;No &lt;code&gt;CTE Scan&lt;&#x2F;code&gt;. The planner inlines the CTE and merges both predicates, just like Case 1. &lt;code&gt;now()&lt;&#x2F;code&gt; is &lt;code&gt;STABLE&lt;&#x2F;code&gt; as it returns the same value within a transaction - and the planner&#x27;s inlining check only looks for &lt;code&gt;VOLATILE&lt;&#x2F;code&gt; functions (via &lt;code&gt;contain_volatile_functions()&lt;&#x2F;code&gt;). &lt;code&gt;STABLE&lt;&#x2F;code&gt; passes that check.&lt;&#x2F;p&gt;
&lt;div class=&quot;callout&quot;&gt;
&lt;p&gt;Why do people think &lt;code&gt;STABLE&lt;&#x2F;code&gt; blocks inlining? Because before PostgreSQL 12, &lt;strong&gt;all&lt;&#x2F;strong&gt; CTEs were materialized regardless of volatility. When PG 12 introduced inlining, the only function-level barrier was &lt;code&gt;VOLATILE&lt;&#x2F;code&gt;. But the old &quot;CTEs are optimization fences&quot; mental model was so deeply ingrained that many developers assumed &lt;code&gt;STABLE&lt;&#x2F;code&gt; was also a problem. It isn&#x27;t.&lt;&#x2F;p&gt;
&lt;p&gt;The function that actually blocks inlining: &lt;code&gt;clock_timestamp()&lt;&#x2F;code&gt;. Unlike &lt;code&gt;now()&lt;&#x2F;code&gt;, it&#x27;s &lt;code&gt;VOLATILE&lt;&#x2F;code&gt; and returns a different value on every call. A CTE with &lt;code&gt;clock_timestamp()&lt;&#x2F;code&gt; will be materialized. Similarly, &lt;code&gt;random()&lt;&#x2F;code&gt; and &lt;code&gt;nextval()&lt;&#x2F;code&gt; are &lt;code&gt;VOLATILE&lt;&#x2F;code&gt; and force materialization (as shown in Case 5).&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;If you see a CTE with &lt;code&gt;now()&lt;&#x2F;code&gt; being materialized, the cause is something else. Either multiple references, a data-modifying statement, or an explicit &lt;code&gt;MATERIALIZED&lt;&#x2F;code&gt; hint. Don&#x27;t blame &lt;code&gt;STABLE&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;case-7-forcing-behaviour-with-hints&quot;&gt;Case 7: forcing behaviour with hints&lt;a class=&quot;zola-anchor&quot; href=&quot;#case-7-forcing-behaviour-with-hints&quot; aria-label=&quot;Anchor link for: case-7-forcing-behaviour-with-hints&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;You can always override the planner&#x27;s decision.&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;-- force materialization on something that would normally be inlined&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;WITH&lt;&#x2F;span&gt;&lt;span&gt; filtered &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; MATERIALIZED (&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; 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;&#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;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; filtered &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; amount &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; 400&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; CTE Scan on filtered  (cost=2105.00..2670.58 rows=5290 width=92)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Filter: (amount &amp;gt; &amp;#39;400&amp;#39;::numeric)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   CTE filtered&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     -&amp;gt;  Seq Scan on orders  (cost=0.00..2105.00 rows=25137 width=59)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           Filter: (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;(5 rows)&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;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- force inlining on something that would normally be materialized&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;WITH&lt;&#x2F;span&gt;&lt;span&gt; filtered &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS NOT&lt;&#x2F;span&gt;&lt;span&gt; MATERIALIZED (&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; 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;&#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;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; filtered a&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;JOIN&lt;&#x2F;span&gt;&lt;span&gt; filtered b &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; a&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;customer_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; b&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;customer_id&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; Hash Join  (cost=2419.21..10686.65 rows=317742 width=118)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Hash Cond: (orders.customer_id = orders_1.customer_id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   -&amp;gt;  Seq Scan on orders  (cost=0.00..2105.00 rows=25137 width=59)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         Filter: (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;  Hash  (cost=2105.00..2105.00 rows=25137 width=59)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         -&amp;gt;  Seq Scan on orders orders_1  (cost=0.00..2105.00 rows=25137 width=59)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;               Filter: (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;(7 rows)    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If the CTE were materialized, you would see a single scan of the &lt;code&gt;orders&lt;&#x2F;code&gt; table, followed by two CTE Scans on the result. Instead, the planner has treated your query as if you had written a standard join between two subqueries.&lt;&#x2F;p&gt;
&lt;div class=&quot;callout&quot;&gt;
&lt;p&gt;&lt;strong&gt;Be careful with &lt;code&gt;NOT MATERIALIZED&lt;&#x2F;code&gt; on multiply-referenced CTEs.&lt;&#x2F;strong&gt; When you force inlining, the subquery runs once for each reference. In the example above, the &lt;code&gt;orders&lt;&#x2F;code&gt; table is scanned twice. Once for &lt;code&gt;a&lt;&#x2F;code&gt; and once for &lt;code&gt;b&lt;&#x2F;code&gt;. For small result sets this might be fine. For large ones, you&#x27;re doing double the work. Measure before using.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;h3 id=&quot;case-8-for-update-for-share-materialized&quot;&gt;Case 8: FOR UPDATE &#x2F; FOR SHARE (MATERIALIZED)&lt;a class=&quot;zola-anchor&quot; href=&quot;#case-8-for-update-for-share-materialized&quot; aria-label=&quot;Anchor link for: case-8-for-update-for-share-materialized&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Row-locking clauses force materialization even on a singly-referenced, side-effect-free CTE. Internally, the planner&#x27;s &lt;code&gt;contain_dml()&lt;&#x2F;code&gt; check treats &lt;code&gt;FOR UPDATE&lt;&#x2F;code&gt; and &lt;code&gt;FOR SHARE&lt;&#x2F;code&gt; the same as data-modifying statements.&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;WITH&lt;&#x2F;span&gt;&lt;span&gt; locked &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; 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 style=&quot;color: #F97583;&quot;&gt; FOR UPDATE&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;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; locked &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; amount &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; 400&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; CTE Scan on locked  (cost=2356.37..2921.95 rows=5290 width=92)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Filter: (amount &amp;gt; &amp;#39;400&amp;#39;::numeric)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   CTE locked&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     -&amp;gt;  LockRows  (cost=0.00..2356.37 rows=25137 width=65)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           -&amp;gt;  Seq Scan on orders  (cost=0.00..2105.00 rows=25137 width=65)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                 Filter: (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;(6 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Without &lt;code&gt;FOR UPDATE&lt;&#x2F;code&gt;, this CTE would be inlined. The &lt;code&gt;LockRows&lt;&#x2F;code&gt; node and &lt;code&gt;CTE Scan&lt;&#x2F;code&gt; confirm materialization.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;decision-matrix&quot;&gt;Decision matrix&lt;a class=&quot;zola-anchor&quot; href=&quot;#decision-matrix&quot; aria-label=&quot;Anchor link for: decision-matrix&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Here&#x27;s the full picture across PostgreSQL versions:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Condition&lt;&#x2F;th&gt;&lt;th&gt;PG ≤ 11&lt;&#x2F;th&gt;&lt;th&gt;PG 12–16&lt;&#x2F;th&gt;&lt;th&gt;PG 17–18&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Single ref, pure SELECT&lt;&#x2F;td&gt;&lt;td&gt;Materialized&lt;&#x2F;td&gt;&lt;td&gt;Inlined&lt;&#x2F;td&gt;&lt;td&gt;Inlined&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Multiple refs, pure SELECT&lt;&#x2F;td&gt;&lt;td&gt;Materialized&lt;&#x2F;td&gt;&lt;td&gt;Materialized&lt;&#x2F;td&gt;&lt;td&gt;Materialized (better stats)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;VOLATILE function&lt;&#x2F;td&gt;&lt;td&gt;Materialized&lt;&#x2F;td&gt;&lt;td&gt;Materialized&lt;&#x2F;td&gt;&lt;td&gt;Materialized&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;STABLE function&lt;&#x2F;td&gt;&lt;td&gt;Materialized&lt;&#x2F;td&gt;&lt;td&gt;Inlined&lt;&#x2F;td&gt;&lt;td&gt;Inlined&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Data-modifying (DML)&lt;&#x2F;td&gt;&lt;td&gt;Materialized&lt;&#x2F;td&gt;&lt;td&gt;Materialized&lt;&#x2F;td&gt;&lt;td&gt;Materialized&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;FOR UPDATE &#x2F; FOR SHARE&lt;&#x2F;td&gt;&lt;td&gt;Materialized&lt;&#x2F;td&gt;&lt;td&gt;Materialized&lt;&#x2F;td&gt;&lt;td&gt;Materialized&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Recursive&lt;&#x2F;td&gt;&lt;td&gt;Materialized&lt;&#x2F;td&gt;&lt;td&gt;Materialized&lt;&#x2F;td&gt;&lt;td&gt;Materialized&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Explicit &lt;code&gt;MATERIALIZED&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;-&lt;&#x2F;td&gt;&lt;td&gt;Materialized&lt;&#x2F;td&gt;&lt;td&gt;Materialized&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Explicit &lt;code&gt;NOT MATERIALIZED&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;-&lt;&#x2F;td&gt;&lt;td&gt;Inlined&lt;&#x2F;td&gt;&lt;td&gt;Inlined&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;the-statistics-black-hole&quot;&gt;The statistics black hole&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-statistics-black-hole&quot; aria-label=&quot;Anchor link for: the-statistics-black-hole&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;As mentioned in &lt;a href=&quot;&#x2F;posts&#x2F;postgresql-statistics&#x2F;&quot;&gt;PostgreSQL Statistics: Why queries run slow&lt;&#x2F;a&gt;, materialized CTEs are one of the places &quot;where no statistics go.&quot; This is arguably the biggest practical problem with CTE materialization.&lt;&#x2F;p&gt;
&lt;p&gt;When the planner materializes a CTE, the result set is stored in a temporary tuplestore. This tuplestore has no &lt;code&gt;pg_statistic&lt;&#x2F;code&gt; entries - no histograms, no MCVs, no correlation data. The planner has to estimate row counts and value distributions using hardcoded defaults.&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s see this in action. Here&#x27;s a CTE over 10,000 rows:&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;WITH&lt;&#x2F;span&gt;&lt;span&gt; all_orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; MATERIALIZED (&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; orders&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;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; all_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 style=&quot;color: #F97583;&quot;&gt; AND&lt;&#x2F;span&gt;&lt;span&gt; amount &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; 400&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; CTE Scan on all_orders  (cost=1855.00..4355.00 rows=5290 width=92)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Filter: ((amount &amp;gt; &amp;#39;400&amp;#39;::numeric) AND (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;   CTE all_orders&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     -&amp;gt;  Seq Scan on orders  (cost=0.00..1855.00 rows=100000 width=59)&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;The planner estimated 5,290 rows. Where does that number come from? The planner has no MCV list for &lt;code&gt;status&lt;&#x2F;code&gt; inside the CTE, no histogram for &lt;code&gt;amount&lt;&#x2F;code&gt;. It falls back to default selectivities of &lt;code&gt;0.3333&lt;&#x2F;code&gt; for the range comparison on &lt;code&gt;amount&lt;&#x2F;code&gt; and a rough guess for the equality on &lt;code&gt;status&lt;&#x2F;code&gt;, and multiplies them against the 100,000 input rows.&lt;&#x2F;p&gt;
&lt;div class=&quot;sidenote&quot;&gt;If this CTE were inlined, the planner would read the actual statistics from &lt;code&gt;pg_statistic&lt;&#x2F;code&gt; for the &lt;code&gt;orders&lt;&#x2F;code&gt; table and produce estimates based on real data distribution, not defaults.&lt;&#x2F;div&gt;
&lt;p&gt;In a simple query this might not matter much. But when a materialized CTE feeds into a join, default-based estimates can cascade. The planner might choose a nested loop where a hash join would be better, or vice versa. It might underestimate memory needs and spill to disk unexpectedly.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pg-17-statistics-propagation&quot;&gt;PG 17: Statistics propagation&lt;a class=&quot;zola-anchor&quot; href=&quot;#pg-17-statistics-propagation&quot; aria-label=&quot;Anchor link for: pg-17-statistics-propagation&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;PostgreSQL 17 brought two significant improvements to materialized CTEs:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Column statistics propagation.&lt;&#x2F;strong&gt; When the planner creates a &lt;code&gt;CTE Scan&lt;&#x2F;code&gt; node, it now propagates column statistics from the underlying query into the scan node. This means &lt;code&gt;n_distinct&lt;&#x2F;code&gt;, MCV lists, and histograms from the source table can inform estimates on the CTE scan.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Path key propagation.&lt;&#x2F;strong&gt; Materialized CTEs now preserve sort order information. If the CTE&#x27;s subquery produces sorted output, the planner knows about it and can skip redundant sorts downstream.&lt;&#x2F;p&gt;
&lt;p&gt;These improvements significantly reduce the estimation gap, but they don&#x27;t eliminate it. Inlined CTEs are still strictly better for planning accuracy, because the planner works directly with the base table statistics rather than propagated copies. If your CTE doesn&#x27;t need to be materialized, don&#x27;t force it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;when-materialization-helps&quot;&gt;When materialization helps&lt;a class=&quot;zola-anchor&quot; href=&quot;#when-materialization-helps&quot; aria-label=&quot;Anchor link for: when-materialization-helps&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Materialization isn&#x27;t always bad.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Multiple references.&lt;&#x2F;strong&gt; If the CTE result is used in multiple places, materialization computes it once. Without it, the subquery runs once per reference.&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;WITH&lt;&#x2F;span&gt;&lt;span&gt; monthly_totals &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    SELECT&lt;&#x2F;span&gt;&lt;span&gt; date_trunc(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;month&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, created_at) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS month&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&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;           sum&lt;&#x2F;span&gt;&lt;span&gt;(amount) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; total&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; orders&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    GROUP BY&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;2&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;SELECT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; cur&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;month&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;cur&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;status&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;cur&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;total&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;       prev&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;total&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS&lt;&#x2F;span&gt;&lt;span&gt; prev_month_total,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;       cur&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;total&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; prev&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;total&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS&lt;&#x2F;span&gt;&lt;span&gt; delta&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; monthly_totals cur&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LEFT JOIN&lt;&#x2F;span&gt;&lt;span&gt; monthly_totals prev&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; cur&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;month&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; prev&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;month&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span&gt; interval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;1 month&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    AND&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; cur&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;status&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; prev&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;status&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; Merge Left Join  (cost=3887.46..3990.90 rows=4384 width=136)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Merge Cond: ((cur.month = ((prev.month + &amp;#39;1 mon&amp;#39;::interval))) AND (cur.status = prev.status))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   CTE monthly_totals&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     -&amp;gt;  HashAggregate  (cost=3105.00..3181.72 rows=4384 width=49)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           Group Key: date_trunc(&amp;#39;month&amp;#39;::text, (orders.created_at)::timestamp with time zone), orders.status&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           -&amp;gt;  Seq Scan on orders  (cost=0.00..2355.00 rows=100000 width=23)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   -&amp;gt;  Sort  (cost=352.87..363.83 rows=4384 width=72)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         Sort Key: cur.month, cur.status&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         -&amp;gt;  CTE Scan on monthly_totals cur  (cost=0.00..87.68 rows=4384 width=72)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   -&amp;gt;  Sort  (cost=352.87..363.83 rows=4384 width=72)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         Sort Key: ((prev.month + &amp;#39;1 mon&amp;#39;::interval)), prev.status&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         -&amp;gt;  CTE Scan on monthly_totals prev  (cost=0.00..87.68 rows=4384 width=72)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(12 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The aggregation runs once. Both &lt;code&gt;cur&lt;&#x2F;code&gt; and &lt;code&gt;prev&lt;&#x2F;code&gt; read from the materialized result. Without materialization, the entire aggregation would run twice.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Expensive VOLATILE expressions.&lt;&#x2F;strong&gt; If a CTE contains calls to volatile functions or expensive computations, materialization ensures they execute exactly once.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Data-modifying operations.&lt;&#x2F;strong&gt; The whole point of writable CTEs is that the side effects happen once and the &lt;code&gt;RETURNING&lt;&#x2F;code&gt; data is available downstream. Materialization is not optional here.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;when-inlining-isn-t-enough&quot;&gt;When inlining isn&#x27;t enough&lt;a class=&quot;zola-anchor&quot; href=&quot;#when-inlining-isn-t-enough&quot; aria-label=&quot;Anchor link for: when-inlining-isn-t-enough&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The imperative mindset from the introduction, &quot;first do this, then do that&quot;, doesn&#x27;t go away just because the planner inlines your CTEs. And this one is my personal favourite, and source that never stop delivering query refactoring.&lt;&#x2F;p&gt;
&lt;p&gt;Developers still structure queries as sequential pipelines, and that structure itself can create performance problems that have nothing to do with materialization.&lt;&#x2F;p&gt;
&lt;p&gt;A common pattern is building queries as an assembly line: one CTE filters rows, the next LEFT JOINs related tables and aggregates metadata with &lt;code&gt;GROUP BY&lt;&#x2F;code&gt;, the next filters on the aggregated results. It reads like a clean pipeline, but the &lt;code&gt;GROUP BY&lt;&#x2F;code&gt; in the middle creates a wall the planner can&#x27;t optimize past.&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;WITH&lt;&#x2F;span&gt;&lt;span&gt; recent_orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; 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-01-01&amp;#39;&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;order_metadata &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    SELECT&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&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;        bool_or(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;oa&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; IS NOT NULL&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; was_archived,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        count&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;o2&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&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; related_count&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; recent_orders o&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    LEFT JOIN&lt;&#x2F;span&gt;&lt;span&gt; orders_archive oa &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; oa&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    LEFT JOIN&lt;&#x2F;span&gt;&lt;span&gt; orders o2 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;customer_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o2&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;customer_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AND&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o2&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; !=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    GROUP BY&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&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;SELECT&lt;&#x2F;span&gt;&lt;span&gt; o.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;m&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;was_archived&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;m&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;related_count&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; recent_orders o&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;JOIN&lt;&#x2F;span&gt;&lt;span&gt; order_metadata m &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; m&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; m&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;was_archived&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;span style=&quot;color: #F97583;&quot;&gt;  AND&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; m&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;related_count&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; 0&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;Each CTE here is referenced once, so they all get inlined. No materialization, no optimization fence. The planner sees the full query. So what&#x27;s the problem?&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;code&gt;GROUP BY&lt;&#x2F;code&gt; in &lt;code&gt;order_metadata&lt;&#x2F;code&gt;. Even after inlining, the planner cannot push the &lt;code&gt;was_archived = false&lt;&#x2F;code&gt; predicate past the aggregation. It must first LEFT JOIN every filtered order against &lt;code&gt;orders_archive&lt;&#x2F;code&gt; and self-join &lt;code&gt;orders&lt;&#x2F;code&gt;, compute the aggregates for all of them, and only then discard the rows that don&#x27;t match. If &lt;code&gt;recent_orders&lt;&#x2F;code&gt; returns 50,000 rows but only 200 were ever archived, you&#x27;re joining and aggregating 49,800 rows for nothing.&lt;&#x2F;p&gt;
&lt;p&gt;The fix is to replace the aggregation-then-filter pattern with correlated &lt;code&gt;EXISTS&lt;&#x2F;code&gt; subqueries:&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; o.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&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&gt; orders o&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&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-01-01&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  AND NOT EXISTS&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;    SELECT&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; FROM&lt;&#x2F;span&gt;&lt;span&gt; orders_archive oa &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; oa&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&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;  AND EXISTS&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;    SELECT&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; FROM&lt;&#x2F;span&gt;&lt;span&gt; orders o2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    WHERE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o2&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;customer_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;customer_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AND&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o2&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; !=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&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;&lt;code&gt;EXISTS&lt;&#x2F;code&gt; short-circuits after finding the first matching row. The planner can push &lt;code&gt;created_at &amp;gt; &#x27;2024-01-01&#x27;&lt;&#x2F;code&gt; all the way down to an index scan on &lt;code&gt;orders&lt;&#x2F;code&gt;, then probe each related table per result. No aggregation, no wasted work.&lt;&#x2F;p&gt;
&lt;div class=&quot;callout&quot;&gt;
&lt;p&gt;&lt;strong&gt;The rule of thumb:&lt;&#x2F;strong&gt; if your CTE contains a &lt;code&gt;GROUP BY&lt;&#x2F;code&gt; or a &lt;code&gt;LEFT JOIN&lt;&#x2F;code&gt; just to compute a boolean (&quot;does this row have related data?&quot;), you&#x27;ve built a wall the planner can&#x27;t see past. A correlated &lt;code&gt;EXISTS&lt;&#x2F;code&gt; lets the planner push filters down and stop scanning early. This applies whether the CTE is materialized or inlined.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;writable-ctes-the-power-and-the-traps&quot;&gt;Writable CTEs (the power and the traps)&lt;a class=&quot;zola-anchor&quot; href=&quot;#writable-ctes-the-power-and-the-traps&quot; aria-label=&quot;Anchor link for: writable-ctes-the-power-and-the-traps&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Data-modifying CTEs let you &lt;code&gt;INSERT&lt;&#x2F;code&gt;, &lt;code&gt;UPDATE&lt;&#x2F;code&gt;, or &lt;code&gt;DELETE&lt;&#x2F;code&gt; inside a &lt;code&gt;WITH&lt;&#x2F;code&gt; clause and use the &lt;code&gt;RETURNING&lt;&#x2F;code&gt; data in subsequent CTEs or the main query.&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;WITH&lt;&#x2F;span&gt;&lt;span&gt; deleted &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    DELETE FROM&lt;&#x2F;span&gt;&lt;span&gt; orders&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&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;cancelled&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;      AND&lt;&#x2F;span&gt;&lt;span&gt; created_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2023-01-01&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    RETURNING &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;archived &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    INSERT INTO&lt;&#x2F;span&gt;&lt;span&gt; orders_archive&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; deleted&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    RETURNING id&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;SELECT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; count&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;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; archived;&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; Aggregate  (cost=2709.19..2709.20 rows=1 width=8)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   CTE deleted&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     -&amp;gt;  Delete on orders  (cost=0.00..2355.00 rows=8334 width=6)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           -&amp;gt;  Seq Scan on orders  (cost=0.00..2355.00 rows=8334 width=6)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                 Filter: ((created_at &amp;lt; &amp;#39;2023-01-01&amp;#39;::date) AND (status = &amp;#39;cancelled&amp;#39;::text))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   CTE archived&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     -&amp;gt;  Insert on orders_archive  (cost=0.00..166.68 rows=8334 width=92)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           -&amp;gt;  CTE Scan on deleted  (cost=0.00..166.68 rows=8334 width=92)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   -&amp;gt;  CTE Scan on archived  (cost=0.00..166.68 rows=8334 width=0)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(9 rows)  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This deletes old cancelled orders, moves them to an archive table, and counts how many were archived - all in a single atomic statement, no application-level coordination needed.&lt;&#x2F;p&gt;
&lt;p&gt;But there are sharp edges.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;you-cannot-read-what-you-just-wrote&quot;&gt;You cannot read what you just wrote&lt;a class=&quot;zola-anchor&quot; href=&quot;#you-cannot-read-what-you-just-wrote&quot; aria-label=&quot;Anchor link for: you-cannot-read-what-you-just-wrote&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;All sub-statements in a data-modifying CTE see the same snapshot. This means the effects of one CTE are &lt;strong&gt;not visible&lt;&#x2F;strong&gt; to other CTEs or the main query when they read the &lt;em&gt;target table&lt;&#x2F;em&gt;. Only the &lt;code&gt;RETURNING&lt;&#x2F;code&gt; clause communicates data between CTE steps.&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 style=&quot;color: #79B8FF;&quot;&gt; count&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: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; customer_id &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;&#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; count&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;    31&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;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;WITH&lt;&#x2F;span&gt;&lt;span&gt; ins &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    INSERT INTO&lt;&#x2F;span&gt;&lt;span&gt; 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;    VALUES&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;100&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;00&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;pending&amp;#39;&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;    RETURNING id&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: #6A737D;&quot;&gt;-- this does NOT see the row we just inserted&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 style=&quot;color: #79B8FF;&quot;&gt; count&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: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; customer_id &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;&#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; count&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;    31&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;The &lt;code&gt;count(1)&lt;&#x2F;code&gt; query sees the pre-insert snapshot. If you need the inserted data, you must use the &lt;code&gt;RETURNING&lt;&#x2F;code&gt; clause from the &lt;code&gt;ins&lt;&#x2F;code&gt; CTE, not re-read the table.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tuple-shuffling&quot;&gt;Tuple shuffling&lt;a class=&quot;zola-anchor&quot; href=&quot;#tuple-shuffling&quot; aria-label=&quot;Anchor link for: tuple-shuffling&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;A common pattern is using writable CTEs to move rows between tables atomically:&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;WITH&lt;&#x2F;span&gt;&lt;span&gt; moved &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    DELETE FROM&lt;&#x2F;span&gt;&lt;span&gt; orders_staging&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    RETURNING &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&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;&#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; orders&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; moved;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This deletes all rows from the staging table and inserts them into the production table in a single atomic operation. No window where data exists in both or neither table.&lt;&#x2F;p&gt;
&lt;div class=&quot;sidenote&quot;&gt;Data-modifying CTEs disable parallel query for the entire statement. If you have a complex query that mixes reads and writes, the write CTE prevents parallelism even for the read-only parts.&lt;&#x2F;div&gt;
&lt;h2 id=&quot;recursive-ctes-are-always-materialized&quot;&gt;Recursive CTEs are always materialized&lt;a class=&quot;zola-anchor&quot; href=&quot;#recursive-ctes-are-always-materialized&quot; aria-label=&quot;Anchor link for: recursive-ctes-are-always-materialized&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Recursive CTEs use an iterative working-table mechanism. Despite the name, they aren&#x27;t truly recursive. PostgreSQL doesn&#x27;t &quot;call itself&quot; by creating a nested stack of unfinished queries. Instead, it operates in loops.&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Execute the non-recursive term (the &quot;seed&quot;). Put results in the working table.&lt;&#x2F;li&gt;
&lt;li&gt;Execute the recursive term using the working table as input. New rows become the &lt;em&gt;next&lt;&#x2F;em&gt; working table.&lt;&#x2F;li&gt;
&lt;li&gt;Repeat until the recursive term returns no new rows.&lt;&#x2F;li&gt;
&lt;li&gt;Return the union of all iterations.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&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;WITH RECURSIVE&lt;&#x2F;span&gt;&lt;span&gt; org_chart &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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: #6A737D;&quot;&gt;    -- Seed: start from the CEO&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; id, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span&gt;, manager_id, &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; AS&lt;&#x2F;span&gt;&lt;span&gt; depth&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; employees&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    WHERE&lt;&#x2F;span&gt;&lt;span&gt; manager_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IS NULL&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;    UNION ALL&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;    -- Recursive term: find direct reports&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 style=&quot;color: #79B8FF;&quot;&gt; e&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;manager_id&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;oc&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;depth&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;&#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; employees e&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    JOIN&lt;&#x2F;span&gt;&lt;span&gt; org_chart oc &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; e&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;manager_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; oc&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&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;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; org_chart &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ORDER BY&lt;&#x2F;span&gt;&lt;span&gt; depth, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;name&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; id |  name   | manager_id | depth&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;  1 | Alice   |            |     1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  2 | Bob     |          1 |     2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  3 | Charlie |          1 |     2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  4 | Diana   |          2 |     3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  5 | Eve     |          2 |     3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  6 | Frank   |          3 |     3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  7 | Grace   |          3 |     3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  8 | Hank    |          6 |     4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  9 | Ivy     |          6 |     4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(9 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;union-vs-union-all&quot;&gt;UNION vs UNION ALL&lt;a class=&quot;zola-anchor&quot; href=&quot;#union-vs-union-all&quot; aria-label=&quot;Anchor link for: union-vs-union-all&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;The choice between &lt;code&gt;UNION&lt;&#x2F;code&gt; and &lt;code&gt;UNION ALL&lt;&#x2F;code&gt; in recursive CTEs matters more than in regular queries.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;UNION ALL&lt;&#x2F;code&gt; keeps all rows, including duplicates. This is faster but dangerous: if your graph has cycles, the recursion never terminates. PostgreSQL will run until you cancel the query or memory is exhausted.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;UNION&lt;&#x2F;code&gt; deduplicates at each iteration. This prevents infinite loops in cyclic graphs but adds the cost of hashing and comparing rows at every step.&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL 14 added SQL-standard &lt;code&gt;SEARCH&lt;&#x2F;code&gt; and &lt;code&gt;CYCLE&lt;&#x2F;code&gt; clauses that replace the manual patterns for controlling traversal order (breadth-first vs. depth-first) and detecting cycles. &lt;code&gt;SEARCH BREADTH FIRST BY&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;SEARCH DEPTH FIRST BY&lt;&#x2F;code&gt; control the order, while &lt;code&gt;CYCLE&lt;&#x2F;code&gt; automatically detects and marks cycles, which is far cleaner than the old pattern of accumulating an array of visited IDs.&lt;&#x2F;p&gt;
&lt;div class=&quot;callout&quot;&gt;
&lt;p&gt;For pure hierarchical data (trees without cycles), consider the &lt;code&gt;ltree&lt;&#x2F;code&gt; extension as an alternative to recursive CTEs. It stores the full path as a label tree and supports efficient ancestor&#x2F;descendant queries with GiST indexes. The trade-off is denormalized storage vs. on-the-fly recursion.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;the-exotic-edge-cases&quot;&gt;The exotic edge cases&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-exotic-edge-cases&quot; aria-label=&quot;Anchor link for: the-exotic-edge-cases&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;partition-pruning-lost&quot;&gt;Partition pruning lost&lt;a class=&quot;zola-anchor&quot; href=&quot;#partition-pruning-lost&quot; aria-label=&quot;Anchor link for: partition-pruning-lost&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;When you materialize a CTE over a partitioned table, partition pruning cannot happen on the CTE scan side. The materialized result is a flat tuplestore disconnected from the partition metadata.&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;-- assume orders is range-partitioned by 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;WITH&lt;&#x2F;span&gt;&lt;span&gt; recent &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; MATERIALIZED (&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; orders&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;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; recent &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;2025-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;p&gt;The &lt;code&gt;created_at &amp;gt; &#x27;2025-06-01&#x27;&lt;&#x2F;code&gt; predicate is applied &lt;em&gt;after&lt;&#x2F;em&gt; materialization. All partitions are scanned to build the CTE, even though only one or two would be needed. Use &lt;code&gt;NOT MATERIALIZED&lt;&#x2F;code&gt; (or simply let the planner inline it) to preserve partition pruning.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;prepared-statements-and-plan-caching&quot;&gt;Prepared statements and plan caching&lt;a class=&quot;zola-anchor&quot; href=&quot;#prepared-statements-and-plan-caching&quot; aria-label=&quot;Anchor link for: prepared-statements-and-plan-caching&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;PostgreSQL generates custom plans for the first 5 executions of a prepared statement. After that, it may switch to a generic plan. CTE inlining decisions can differ between custom and generic plans, because generic plans don&#x27;t know the actual parameter values.&lt;&#x2F;p&gt;
&lt;p&gt;This means a CTE that gets inlined during your first 5 calls might start materializing on the 6th or vice versa. If you see sudden plan changes with prepared statements, check whether the CTE inlining behaviour has shifted.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;work-mem-spilling&quot;&gt;work_mem spilling&lt;a class=&quot;zola-anchor&quot; href=&quot;#work-mem-spilling&quot; aria-label=&quot;Anchor link for: work-mem-spilling&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Materialized CTEs store their results in memory, bounded by &lt;code&gt;work_mem&lt;&#x2F;code&gt;. When the result set exceeds this limit, it silently spills to disk as a temporary file. This is not an error - it just gets slower.&lt;&#x2F;p&gt;
&lt;p&gt;Monitor with &lt;code&gt;log_temp_files = 0&lt;&#x2F;code&gt; (logs all temp files) or check &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;&#x2F;code&gt; for temp read&#x2F;write counts.&lt;&#x2F;p&gt;
&lt;div class=&quot;callout&quot;&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL 18: EXPLAIN now shows memory&#x2F;disk usage&lt;&#x2F;strong&gt;&lt;br&gt;
Starting with PostgreSQL 18, &lt;code&gt;EXPLAIN ANALYZE&lt;&#x2F;code&gt; reports memory and disk usage for Material nodes, including CTE materialization. You can see exactly how much memory a materialized CTE consumed and whether it spilled to disk.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;h3 id=&quot;cte-and-security-barrier-views&quot;&gt;CTE and security barrier views&lt;a class=&quot;zola-anchor&quot; href=&quot;#cte-and-security-barrier-views&quot; aria-label=&quot;Anchor link for: cte-and-security-barrier-views&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;div class=&quot;sidenote&quot;&gt;A `security_barrier` view is a &quot;black box&quot; that forces the database to fully resolve the view&#x27;s internal logic before any outer filters are applied.&lt;&#x2F;div&gt;
Security-barrier views already prevent subquery flattening as a security measure (to stop user-defined functions from seeing rows they shouldn&#x27;t). When you combine a security-barrier view with a CTE, you compound the optimisation barriers. The planner can neither inline the view nor the CTE. If performance matters in this scenario, consider materializing the security-sensitive filtering into a temporary table first.
&lt;h2 id=&quot;cte-vs-subquery-vs-temporary-table&quot;&gt;CTE vs. subquery vs. temporary table&lt;a class=&quot;zola-anchor&quot; href=&quot;#cte-vs-subquery-vs-temporary-table&quot; aria-label=&quot;Anchor link for: cte-vs-subquery-vs-temporary-table&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;CTE, referenced once&lt;&#x2F;strong&gt; the planner inlines it on PG 12+, so it doesn&#x27;t affect the execution plan. This is the default choice for breaking up complex queries.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;CTE, referenced multiple times (small result)&lt;&#x2F;strong&gt; represents acceptable cost. Materialization means the subquery runs once. For aggregations or filtered subsets that produce a few hundred rows, the overhead is minimal.&lt;&#x2F;p&gt;
&lt;div class=&quot;sidenote&quot;&gt;As &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;hettie-d&quot;&gt;Henrietta Dombrovskaya&lt;&#x2F;a&gt; emphasizes, &quot;The best temporary table is the one you didn&#x27;t create&quot;. Always exhaust your indexing and query-rewriting options before reaching for a temp table, as the DDL overhead often outweighs the execution gains.&lt;&#x2F;div&gt;
**CTE, referenced multiple times (large result)** ss case when you might consider a temporary table instead. A materialized CTE has no indexes and no statistics. A temporary table can have both, and as covered in [Introduction to Buffers](&#x2F;posts&#x2F;introduction-to-buffers&#x2F;), temporary tables use local buffers with simpler locking and no WAL overhead. If you&#x27;re joining against 100k+ rows from a CTE, create a temp table, add an index, and `ANALYZE` it.
&lt;p&gt;&lt;strong&gt;Data-modifying operations&lt;&#x2F;strong&gt; with writable CTE. No alternative gives you the atomic, single-statement behavior.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Recursive CTEs&lt;&#x2F;strong&gt;. There&#x27;s no alternative in pure SQL.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Large intermediate results needing indexes&#x2F;statistics&lt;&#x2F;strong&gt;. If you really need them use temporary table. As discussed in &lt;a href=&quot;&#x2F;posts&#x2F;explain-buffers&#x2F;&quot;&gt;Reading Buffer statistics&lt;&#x2F;a&gt;, temporary tables offer full planner support - indexes, statistics, and buffer management - that materialized CTEs simply don&#x27;t have.&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Scenario&lt;&#x2F;th&gt;&lt;th&gt;Recommendation&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Readability, single reference&lt;&#x2F;td&gt;&lt;td&gt;CTE (inlined, free)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Compute once, use many times (small)&lt;&#x2F;td&gt;&lt;td&gt;CTE (materialized)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Compute once, use many times (large)&lt;&#x2F;td&gt;&lt;td&gt;Temporary table&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Atomic data modification&lt;&#x2F;td&gt;&lt;td&gt;Writable CTE&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Hierarchy &#x2F; graph traversal&lt;&#x2F;td&gt;&lt;td&gt;Recursive CTE&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Need indexes on intermediate data&lt;&#x2F;td&gt;&lt;td&gt;Temporary table&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h2 id=&quot;the-pg-18-state-of-affairs&quot;&gt;The PG 18 state of affairs&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-pg-18-state-of-affairs&quot; aria-label=&quot;Anchor link for: the-pg-18-state-of-affairs&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;PostgreSQL 18 continues to refine CTE handling without any revolutionary changes:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;EXPLAIN shows memory&#x2F;disk usage&lt;&#x2F;strong&gt; for CTE materialization nodes. You can finally see whether your CTE fit in &lt;code&gt;work_mem&lt;&#x2F;code&gt; or spilled to disk.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Better query plans for CTEs on the same table.&lt;&#x2F;strong&gt; The planner is smarter about eliminating redundant scans when multiple CTEs reference the same underlying table.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Data-modifying CTE fix for updatable views with rules.&lt;&#x2F;strong&gt; An edge case where writable CTEs interacted incorrectly with views defined using rules has been resolved.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;CTE inlining is mature.&lt;&#x2F;strong&gt; The core inlining logic hasn&#x27;t changed since PG 12. What has improved is everything around it - better statistics propagation (PG 17), better materialisation diagnostics (PG 18), better cost estimation.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;CTEs are a good tool. Just know when you&#x27;re holding the sharp end.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <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>PostgreSQL Statistics: Why queries run slow</title>
        <published>2026-02-26T23:35:00+00:00</published>
        <updated>2026-02-26T23:35:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/postgresql-statistics/"/>
        <id>https://boringsql.com/posts/postgresql-statistics/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/postgresql-statistics/">&lt;p&gt;Every query starts with a plan. Every slow query probably starts with a bad one. And more often than not, the statistics are to blame. But how does it really work? PostgreSQL doesn&#x27;t run the query to find out - it estimates the cost. It reads pre-computed data from &lt;code&gt;pg_class&lt;&#x2F;code&gt; and &lt;code&gt;pg_statistic&lt;&#x2F;code&gt; and does the maths to figure out the cheapest path to your data.&lt;&#x2F;p&gt;
&lt;p&gt;In ideal scenario, the numbers read are accurate, and you get the plan you expect. But when they are stale, the situation gets out of control. Planner estimates 500 rows, plans a nested loop, and hits 25,000. What seemed as optimal plan turns into a cascading failure.&lt;&#x2F;p&gt;
&lt;p&gt;How do statistics get stale? It can be either bulk load, a schema migration, faster-than-expected growth, or simply &lt;code&gt;VACUUM&lt;&#x2F;code&gt; not keeping up. Whatever the cause, the result is the same. The planner is flying blind. Choosing paths based on reality that no longer exists.&lt;&#x2F;p&gt;
&lt;p&gt;In this post we will go inside the two catalogs the planner depends on, understand what &lt;code&gt;ANALYZE&lt;&#x2F;code&gt; actually gets for you from a 30,000-row table, and see how those numbers determine whether your query takes milliseconds or minutes.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sample-schema&quot;&gt;Sample schema&lt;a class=&quot;zola-anchor&quot; href=&quot;#sample-schema&quot; aria-label=&quot;Anchor link for: sample-schema&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;For demonstration purposes we will use the same schema as in the article &lt;a href=&quot;&#x2F;posts&#x2F;explain-buffers&#x2F;&quot;&gt;Reading Buffer statistics in EXPLAIN output&lt;&#x2F;a&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;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; customers&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 style=&quot;color: #F97583;&quot;&gt;    name text NOT NULL&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;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; 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 REFERENCES&lt;&#x2F;span&gt;&lt;span&gt; customers(id),&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;    note &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&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; customers (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;name&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;SELECT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Customer &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span&gt; i&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;2000&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; i;&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; orders (customer_id, amount, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;status&lt;&#x2F;span&gt;&lt;span&gt;, note, 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; 1999&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; 500&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: #F97583;&quot;&gt;    CASE WHEN&lt;&#x2F;span&gt;&lt;span&gt; random()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;lt;&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;3&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; THEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Some note text here for padding&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ELSE NULL END&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;2022-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; 1095&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;100000&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&gt;ANALYZE customers;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ANALYZE orders;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;what-the-planner-reads&quot;&gt;What the Planner Reads&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-the-planner-reads&quot; aria-label=&quot;Anchor link for: what-the-planner-reads&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;As mentioned above, every decision the planner makes is based on two sources:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;table-level metadata from &lt;code&gt;pg_class&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;column-level metadata from &lt;code&gt;pg_statistic&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;pg-class-relational-level-stats&quot;&gt;pg_class - relational-level stats&lt;a class=&quot;zola-anchor&quot; href=&quot;#pg-class-relational-level-stats&quot; aria-label=&quot;Anchor link for: pg-class-relational-level-stats&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;div class=&quot;sidenote&quot;&gt;It actually tracks all &lt;i&gt;relations&lt;&#x2F;i&gt;. Not just tables and indexes, but also partitions, TOAST tables, sequences, composite types, and views. &lt;&#x2F;div&gt;Every table, index and materialized view has a row in `pg_class`. Before it even looks at column-level statistics, the planner reads four values from it:
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Column&lt;&#x2F;th&gt;&lt;th&gt;Meaning&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;relpages&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Number of 8KB pages representing the table on the disk&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;reltuples&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Estimated number of live rows in the table&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;relallvisible&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Pages where all tuples are visible to all transactions&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;For our sample table it 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;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, relallvisible&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&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;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 | relallvisible&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; orders  |      856 |    100000 |           856&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;div class=&quot;sidenote&quot;&gt;Compared to reading page from disk, handling single tuple represents tiny overhead. Per-row overhead is configured using &lt;code&gt;cpu_tuple_cost&lt;&#x2F;code&gt;, which defaults to &lt;code&gt;0.01&lt;&#x2F;code&gt;&lt;&#x2F;div&gt;
&lt;p&gt;The planner sees 100,000 rows spread across 856 pages. Every cost estimate starts from those two numbers. &lt;code&gt;relpages&lt;&#x2F;code&gt; drives sequential scan cost - each page is one unit of I&#x2F;O work as configured via &lt;code&gt;seq_page_cost&lt;&#x2F;code&gt;. &lt;code&gt;reltuples&lt;&#x2F;code&gt; controls estimates for joins, aggregations, and pretty much everything else.&lt;&#x2F;p&gt;
&lt;div class=&quot;callout&quot;&gt;
&lt;p&gt;The &lt;code&gt;reltuples&lt;&#x2F;code&gt; value is only an estimate, not a live count. It&#x27;s updated by &lt;code&gt;ANALYZE&lt;&#x2F;code&gt; (and autovacuum), not by individual INSERTs or DELETEs. Between ANALYZE runs, PostgreSQL scales &lt;code&gt;reltuples&lt;&#x2F;code&gt; proportionally when &lt;code&gt;relpages&lt;&#x2F;code&gt; value changes - if the table grows by 10% in pages, the planner assumes 10% more rows too.&lt;&#x2F;p&gt;
&lt;p&gt;This works well enough for normal growth, but breaks down with bloat. If dead tuples are inflating the number of pages used, without adding real rows, the planner overestimates the table size.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;The other column mentioned above matters for specific operations. &lt;code&gt;relallvisible&lt;&#x2F;code&gt; tells the planner how much of the table can be read with an index-only scan. Meaning an index-only scan can return results using the index without checking the heap for visibility.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pg-statistic-via-pg-stats-column-level-stats&quot;&gt;pg_statistic (via pg_stats) - column-level stats&lt;a class=&quot;zola-anchor&quot; href=&quot;#pg-statistic-via-pg-stats-column-level-stats&quot; aria-label=&quot;Anchor link for: pg-statistic-via-pg-stats-column-level-stats&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Knowing the size of a table is only half the picture. To estimate how many rows might match, the planner needs to understand the data inside each column. PostgreSQL maintains statistics in &lt;code&gt;pg_statistic&lt;&#x2F;code&gt; catalog. Which you&#x27;re most likely never going to use directly. In practice you will use the view &lt;code&gt;pg_stats&lt;&#x2F;code&gt; which presents the human-friendly data behind it.&lt;&#x2F;p&gt;
&lt;p&gt;The most interesting values it exposes are:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Statistic&lt;&#x2F;th&gt;&lt;th&gt;What it tells the planner&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;null_frac&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Fraction of entries that are NULL values&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;avg_width&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Average width in bytes&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;n_distinct&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Number of distinct values (negative means fraction of rows)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;most_common_vals&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Most frequent values&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;most_common_freqs&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Frequencies of those values&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;histogram_bounds&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Values dividing the remaining data into equal-population buckets (most_common_vals are excluded)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;correlation&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;Statistical correlation between physical row ordering and logical ordering of the column values&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&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; attname, null_frac, avg_width, n_distinct,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       most_common_vals, most_common_freqs, histogram_bounds, correlation&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_stats&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; tablename &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;orders&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AND&lt;&#x2F;span&gt;&lt;span&gt; attname &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;status&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;-[ RECORD 1 ]-----+--------------------------------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;attname           | status&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;null_frac         | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;avg_width         | 8&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;n_distinct        | 4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;most_common_vals  | {pending,shipped,delivered,cancelled}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;most_common_freqs | {0.25396666,0.25,0.24973333,0.2463}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;histogram_bounds  |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;correlation       | 0.2524199&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;From this planner knows there are exactly 4 distinct values, more or less equally distributed, without any NULL values. When you write a predicate &lt;code&gt;WHERE status = &#x27;pending&#x27;&lt;&#x2F;code&gt; it will estimate ~25% of rows are going to match. All that by not running a query, but reading the catalog row.&lt;&#x2F;p&gt;
&lt;p&gt;Different situation is when checking column &lt;code&gt;note&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; attname, null_frac, avg_width, n_distinct,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       array_length(most_common_vals, &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;AS&lt;&#x2F;span&gt;&lt;span&gt; mcv_count,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       array_length(histogram_bounds, &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;AS&lt;&#x2F;span&gt;&lt;span&gt; histogram_buckets&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_stats&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; tablename &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;orders&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AND&lt;&#x2F;span&gt;&lt;span&gt; attname &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;note&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;-[ RECORD 1 ]-----+-------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;attname           | note&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;null_frac         | 0.6982&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;avg_width         | 32&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;n_distinct        | 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;mcv_count         | 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;histogram_buckets |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For this column there&#x27;s ~70% of NULLs, and only one distinct value. Therefore &lt;code&gt;WHERE note IS NOT NULL&lt;&#x2F;code&gt; will estimate 30% of rows.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;selectivity-in-action&quot;&gt;Selectivity in Action&lt;a class=&quot;zola-anchor&quot; href=&quot;#selectivity-in-action&quot; aria-label=&quot;Anchor link for: selectivity-in-action&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Now that we have covered what data the planner has available, let&#x27;s have a look at how it&#x27;s used to estimate how many rows a certain part of a query will read. This &quot;guess&quot; is called &lt;strong&gt;Selectivity&lt;&#x2F;strong&gt;. And it&#x27;s defined as a floating-point number between 0 and 1.&lt;&#x2F;p&gt;
&lt;p&gt;The formula is pretty simple:&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;  Estimated Rows = Total Rows * Selectivity&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;But the way Selectivity is calculated depends entirely on the operator you use (&lt;code&gt;=&lt;&#x2F;code&gt;, &lt;code&gt;&amp;lt;&lt;&#x2F;code&gt;, &lt;code&gt;&amp;gt;&lt;&#x2F;code&gt; or &lt;code&gt;LIKE&lt;&#x2F;code&gt;).&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-equality-most-common-values&quot;&gt;The equality (Most Common Values)&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-equality-most-common-values&quot; aria-label=&quot;Anchor link for: the-equality-most-common-values&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Equality is easiest to start with. When you use &lt;code&gt;WHERE status = &#x27;shipped&#x27;&lt;&#x2F;code&gt; the planner first checks &lt;code&gt;most_common_vals&lt;&#x2F;code&gt; (MCV) list. If there&#x27;s a match, the selectivity is same as the one provided by &lt;code&gt;most_common_freqs&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;If the value isn&#x27;t in the list, the planner assumes the value is part of the &quot;remaining&quot; population. It subtracts all MCV frequencies from 1.0 and divides the remainder by the number of other distinct values.&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;(1.0 - (SELECT sum(s) FROM unnest(most_common_freqs) s))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(n_distinct - array_length(most_common_vals, 1))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;the-range-lookup-the-histogram&quot;&gt;The Range lookup (The Histogram)&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-range-lookup-the-histogram&quot; aria-label=&quot;Anchor link for: the-range-lookup-the-histogram&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Life would be easy if we would be looking only for exact values. Most of the time we need to utilise range lookup. For example &lt;code&gt;WHERE amount &amp;gt; 400&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;MCVs are useless in this case as there might be thousands or millions of unique constants. This is where &lt;code&gt;histogram_bounds&lt;&#x2F;code&gt; comes in. PostgreSQL divides the column values into a number of buckets, where each bucket contains equal number of rows (not values).&lt;&#x2F;p&gt;
&lt;p&gt;The Selectivity in this case is determined by how many buckets your query covers. In our example, if we have bounds &lt;code&gt;(100, 200, 300, 400, 500, 600)&lt;&#x2F;code&gt; the planner will establish it covers 2 full buckets. Since there are 5 buckets in total the Selectivity is going to be 0.4 (2&#x2F;5).&lt;&#x2F;p&gt;
&lt;p&gt;If you are now wondering where 2 and 5 come from? The &lt;code&gt;histogram_bounds&lt;&#x2F;code&gt; array defines boundaries between buckets. With bounds &lt;code&gt;(100, 200, 300, 400, 500, 600)&lt;&#x2F;code&gt; there are 5 buckets in total.&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;(100-200), (200-300), (300-400), (400-500), (500-600)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And the same logic applies to matching. The condition &lt;code&gt;amount &amp;gt; 400&lt;&#x2F;code&gt; matches on &lt;code&gt;(400-500)&lt;&#x2F;code&gt; and &lt;code&gt;(500-600)&lt;&#x2F;code&gt; buckets (2 in total).&lt;&#x2F;p&gt;
&lt;div class=&quot;sidenote&quot;&gt;The histogram&#x27;s biggest weakness is linear interpolation. If your data has massive spikes in distribution the planner will always assume a perfect distribution.&lt;&#x2F;div&gt;
&lt;p&gt;Slightly more complex situation happens in the cases where your value falls inside a bucket. If your query has &lt;code&gt;WHERE amount &amp;gt; 350&lt;&#x2F;code&gt; the planner locates the bucket containing value 350 (in our example &lt;code&gt;(300-400)&lt;&#x2F;code&gt;), assumes the data is linearly distributed and calculates the ratio within that bucket.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;search-and-pattern-matching&quot;&gt;Search and pattern matching&lt;a class=&quot;zola-anchor&quot; href=&quot;#search-and-pattern-matching&quot; aria-label=&quot;Anchor link for: search-and-pattern-matching&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;This is probably the most treacherous territory for the planner. For substring matching patterns like &lt;code&gt;WHERE note LIKE &#x27;%middle%&#x27;&lt;&#x2F;code&gt;, there&#x27;s no histogram or list of values to rely on. The planner must fall back to &quot;magic constants&quot; hardcoded in the PostgreSQL source code.&lt;&#x2F;p&gt;
&lt;p&gt;The default for generic patterns is 0.5% of the total rows, defined as&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;#define DEFAULT_MATCH_SEL  0.005&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Slightly better situation comes for prefixed matches like &lt;code&gt;WHERE note LIKE &#x27;boringSQL%&#x27;&lt;&#x2F;code&gt; where PostgreSQL can fall back to range conditions and use histogram bounds. While this is a subtle difference, it makes a night and day difference.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;correlation-and-index-scan-cost&quot;&gt;Correlation and index scan cost&lt;a class=&quot;zola-anchor&quot; href=&quot;#correlation-and-index-scan-cost&quot; aria-label=&quot;Anchor link for: correlation-and-index-scan-cost&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Remember &lt;code&gt;correlation&lt;&#x2F;code&gt; from the &lt;code&gt;pg_stats&lt;&#x2F;code&gt;? It says how closely the physical order of rows on disk matches the logical order of column values. Values close to 1.0 mean high correlation, values near 0 mean data is laid out randomly across pages.&lt;&#x2F;p&gt;
&lt;p&gt;If you recall &lt;a href=&quot;&#x2F;posts&#x2F;inside-the-8kb-page&#x2F;&quot;&gt;how data is organized in 8KB page&lt;&#x2F;a&gt; the row locality matters. This matters because it determines whether an index scan is worth it. The planner assumes a random page read costs 4× more than a sequential one (&lt;code&gt;random_page_cost = 4.0&lt;&#x2F;code&gt; vs &lt;code&gt;seq_page_cost = 1.0&lt;&#x2F;code&gt;). When correlation is high, the rows an index points to are physically adjacent.  The planner expects sequential I&#x2F;O and costs the scan cheaply. When correlation is low, each lookup likely hits a different page, and the planner costs each of those reads at the higher random rate. That difference alone can be enough to make a sequential scan cheaper than an index scan.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;n-distinct-and-join-estimation&quot;&gt;n_distinct and join estimation&lt;a class=&quot;zola-anchor&quot; href=&quot;#n-distinct-and-join-estimation&quot; aria-label=&quot;Anchor link for: n-distinct-and-join-estimation&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Value &lt;code&gt;n_distinct&lt;&#x2F;code&gt; plays an important role in joins. This further stresses the importance of running &lt;code&gt;ANALYZE&lt;&#x2F;code&gt; after bulk data changes.&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s imagine this simplified logic for the equality join:&lt;&#x2F;p&gt;
&lt;div class=&quot;sidenote&quot;&gt;In reality, Postgres also accounts for &lt;code&gt;null_frac&lt;&#x2F;code&gt; (NULLs don&#x27;t join) and MCVs on both sides. If both sides have MCVs, it calculates the &quot;inner product&quot; of the frequencies.&lt;&#x2F;div&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;estimated_rows = (rows_left × rows_right) &#x2F; max(n_distinct_left, n_distinct_right)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Let&#x27;s say you&#x27;re trying to join two tables, both having roughly 2,000 distinct values for the join key. Outdated &lt;code&gt;n_distinct&lt;&#x2F;code&gt; will cause significant estimation drifts and the planner may pick a wrong join strategy altogether.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;this-is-the-heap-we-are-talking-about&quot;&gt;This is the heap we are talking about&lt;a class=&quot;zola-anchor&quot; href=&quot;#this-is-the-heap-we-are-talking-about&quot; aria-label=&quot;Anchor link for: this-is-the-heap-we-are-talking-about&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Please, keep in mind this describes how the planner estimates rows for a basic heap scan. Indexes, join selectivity, and complex types like JSONB each come with their own estimation logic, operator handling and quirks. The fundamentals of estimation are the same nevertheless.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-if-there-are-no-statistics&quot;&gt;What if there are no statistics?&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-if-there-are-no-statistics&quot; aria-label=&quot;Anchor link for: what-if-there-are-no-statistics&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;So far we have touched on why up-to-date statistics are a must. But what if there are no statistics at all? For example for a new table or new column when &lt;code&gt;ANALYZE&lt;&#x2F;code&gt; has not yet run.&lt;&#x2F;p&gt;
&lt;p&gt;In those cases PostgreSQL falls back to hardcoded defaults.&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Condition type&lt;&#x2F;th&gt;&lt;th&gt;Default selectivity&lt;&#x2F;th&gt;&lt;th&gt;Constant&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Equality (&lt;code&gt;=&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;td&gt;0.5%&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;DEFAULT_EQ_SEL = 0.005&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Range (&lt;code&gt;&amp;gt;&lt;&#x2F;code&gt;, &lt;code&gt;&amp;lt;&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;td&gt;33.3%&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;DEFAULT_INEQ_SEL = 0.3333&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Range (&lt;code&gt;BETWEEN&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;td&gt;0.5%&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;DEFAULT_RANGE_INEQ_SEL = 0.005&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Pattern matching (&lt;code&gt;LIKE&lt;&#x2F;code&gt;)&lt;&#x2F;td&gt;&lt;td&gt;0.5%&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;DEFAULT_MATCH_SEL = 0.005&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;IS NULL&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;0.5%&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;DEFAULT_UNK_SEL = 0.005&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;IS NOT NULL&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;99.5%&lt;&#x2F;td&gt;&lt;td&gt;&lt;code&gt;DEFAULT_NOT_UNK_SEL = 0.995&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Nothing that a quick &lt;code&gt;ANALYZE&lt;&#x2F;code&gt; can&#x27;t fix, correct? Or maybe not.&lt;&#x2F;p&gt;
&lt;div class=&quot;callout&quot;&gt;
&lt;strong&gt;Where No Statistics Go&lt;&#x2F;strong&gt;&lt;br&#x2F;&gt;
While this article focuses on statistics and getting them right, there are situations where no statistics will be available (never or not predictably). If you believe it won&#x27;t affect you, please, think twice.
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CTEs and subqueries&lt;&#x2F;strong&gt; when not inlined&#x2F;materialized have no statistics. See &lt;a href=&quot;&#x2F;posts&#x2F;good-cte-bad-cte&#x2F;&quot;&gt;Good CTE, Bad CTE&lt;&#x2F;a&gt; for when exactly this happens.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Temporary tables&lt;&#x2F;strong&gt; are not touched by autovacuum, so no automatic &lt;code&gt;ANALYZE&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Foreign tables&lt;&#x2F;strong&gt; do not guarantee stats are propagated.&lt;&#x2F;li&gt;
&lt;li&gt;And, to a big surprise, &lt;strong&gt;computed expressions in WHERE&lt;&#x2F;strong&gt; like &lt;code&gt;WHERE amount * 1.1 &gt; 500&lt;&#x2F;code&gt; or &lt;code&gt;lower(email) = &#x27;hello@example.com&#x27;&lt;&#x2F;code&gt;; unless you create an expression index or extended statistics.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;And btw, do you know about &lt;code&gt;TRUNCATE&lt;&#x2F;code&gt; - it&#x27;s fast way to get rid of the data, but it leaves statistics behind...&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;how-analyze-works&quot;&gt;How &lt;code&gt;ANALYZE&lt;&#x2F;code&gt; Works&lt;a class=&quot;zola-anchor&quot; href=&quot;#how-analyze-works&quot; aria-label=&quot;Anchor link for: how-analyze-works&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;As we have already mentioned several times, &lt;code&gt;ANALYZE&lt;&#x2F;code&gt; is the only mechanism that populates &lt;code&gt;pg_class&lt;&#x2F;code&gt; and &lt;code&gt;pg_statistic&lt;&#x2F;code&gt; with fresh data. Understanding what it samples, what it computes, and what it misses, is key to understanding why statistics are sometimes wrong.&lt;&#x2F;p&gt;
&lt;p&gt;The process itself consists of 6 separate steps (as reported by &lt;code&gt;pg_stat_progress_analyze&lt;&#x2F;code&gt;):&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;initializing&lt;&#x2F;li&gt;
&lt;li&gt;acquiring sample rows&lt;&#x2F;li&gt;
&lt;li&gt;acquiring inherited sample rows (child&#x2F;partitioned tables)&lt;&#x2F;li&gt;
&lt;li&gt;computing statistics (MCVs, histograms, correlation, etc.)&lt;&#x2F;li&gt;
&lt;li&gt;computing extended statistics (see below)&lt;&#x2F;li&gt;
&lt;li&gt;finalizing and writing to &lt;code&gt;pg_statistic&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;For our purposes we will only cover sampling, computing statistics and writing to &lt;code&gt;pg_statistic&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-sampling-mechanism&quot;&gt;The sampling mechanism&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-sampling-mechanism&quot; aria-label=&quot;Anchor link for: the-sampling-mechanism&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;ANALYZE actually doesn&#x27;t read the entire table. It samples what is considered to be a statistically justified minimum sample size (defined as 300 for the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Reservoir_sampling&quot;&gt;reservoir sampling&lt;&#x2F;a&gt; algorithm). For PostgreSQL that means 300 × &lt;code&gt;default_statistics_target&lt;&#x2F;code&gt; rows.&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;SHOW default_statistics_target;&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; default_statistics_target&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; 100&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 the default target of 100, that&#x27;s 30,000 rows. For our 100,000-row orders table, &lt;code&gt;ANALYZE&lt;&#x2F;code&gt; reads about 30% of the data. For a 50-million-row table, it reads 0.06%. The same target also controls the size of the MCV list and histogram. Up to 100 entries each.&lt;&#x2F;p&gt;
&lt;div class=&quot;sidenote&quot;&gt;With a target of 100, don&#x27;t be surprised that &lt;code&gt;histogram_bounds&lt;&#x2F;code&gt; contains 101 values. 100 buckets need 101 boundaries to close them.&lt;&#x2F;div&gt;
&lt;p&gt;The sampling is two-stage. First, &lt;code&gt;ANALYZE&lt;&#x2F;code&gt; selects a random set of pages. Then it reads all live rows from those pages. This gives a representative cross-section without reading every page.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;computing-statistics&quot;&gt;Computing statistics&lt;a class=&quot;zola-anchor&quot; href=&quot;#computing-statistics&quot; aria-label=&quot;Anchor link for: computing-statistics&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Once ANALYZE has its 30,000 sample rows (considering the default values), it processes each column independently. The pipeline for a single column looks roughly like this:&lt;&#x2F;p&gt;
&lt;p&gt;First, it counts NULLs and calculates &lt;code&gt;null_frac&lt;&#x2F;code&gt; and &lt;code&gt;avg_width&lt;&#x2F;code&gt; - the cheapest statistics to compute. Then it sorts the non-null values and builds the MCV list by counting duplicates. Values that appear frequently enough make the cut; the rest are passed to the histogram builder, which divides them into equal-population buckets. Finally, because the values are already sorted, ANALYZE compares the logical sort order against the physical tuple positions (which page each row came from) to compute &lt;code&gt;correlation&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The key detail here is that MCVs and histograms are not built from the same pool. Values that land in &lt;code&gt;most_common_vals&lt;&#x2F;code&gt; are excluded from &lt;code&gt;histogram_bounds&lt;&#x2F;code&gt;. This is why you&#x27;ll sometimes see a column with MCVs but no histogram, or a histogram but no MCVs. They represent different slices of the same data.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;writing-to-pg-statistic&quot;&gt;Writing to pg_statistic&lt;a class=&quot;zola-anchor&quot; href=&quot;#writing-to-pg-statistic&quot; aria-label=&quot;Anchor link for: writing-to-pg-statistic&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Once all columns are processed, ANALYZE writes the results into &lt;code&gt;pg_statistic&lt;&#x2F;code&gt; - one row per column. If a row for that column already exists, it&#x27;s updated in place. This is a regular heap update, which means the old row becomes a dead tuple. On tables with many columns or frequent ANALYZE runs, this can cause &lt;code&gt;pg_statistic&lt;&#x2F;code&gt; itself to bloat.&lt;&#x2F;p&gt;
&lt;p&gt;After &lt;code&gt;pg_statistic&lt;&#x2F;code&gt; is updated, ANALYZE refreshes &lt;code&gt;relpages&lt;&#x2F;code&gt;, &lt;code&gt;reltuples&lt;&#x2F;code&gt;, and &lt;code&gt;relallvisible&lt;&#x2F;code&gt; in &lt;code&gt;pg_class&lt;&#x2F;code&gt;. These values are recalculated from the sampling, not from a full table scan (they are estimates too).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;controlling-statistics-quality&quot;&gt;Controlling statistics quality&lt;a class=&quot;zola-anchor&quot; href=&quot;#controlling-statistics-quality&quot; aria-label=&quot;Anchor link for: controlling-statistics-quality&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;default-statistics-target&quot;&gt;default_statistics_target&lt;a class=&quot;zola-anchor&quot; href=&quot;#default-statistics-target&quot; aria-label=&quot;Anchor link for: default-statistics-target&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;The default target of 100 works well for most columns. It means up to 100 MCVs, 101 histogram bounds, and a 30,000-row sample. Increasing it helps when:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;A column has many distinct values and the top 100 don&#x27;t cover enough of the distribution&lt;&#x2F;li&gt;
&lt;li&gt;Range queries on skewed data produce bad estimates because histogram buckets are too coarse&lt;&#x2F;li&gt;
&lt;li&gt;Join estimates are off because &lt;code&gt;n_distinct&lt;&#x2F;code&gt; is inaccurate&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The cost scales linearly. Setting it to 1000 means 300,000 sampled rows, up to 1000 MCVs, more catalog storage, and slower planning from larger arrays to search. The maximum is 10,000.&lt;&#x2F;p&gt;
&lt;p&gt;You don&#x27;t have to raise it globally. For a single problematic column:&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 TABLE&lt;&#x2F;span&gt;&lt;span&gt; orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ALTER&lt;&#x2F;span&gt;&lt;span&gt; COLUMN &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;status SET STATISTICS&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 500&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 orders;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now &lt;code&gt;status&lt;&#x2F;code&gt; gets up to 500 MCVs and 501 histogram bounds, while every other column stays at 100.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;extended-statistics&quot;&gt;Extended statistics&lt;a class=&quot;zola-anchor&quot; href=&quot;#extended-statistics&quot; aria-label=&quot;Anchor link for: extended-statistics&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Standard statistics treat each column independently. This means the planner can&#x27;t know that &lt;code&gt;city = &#x27;Edinburgh&#x27;&lt;&#x2F;code&gt; and &lt;code&gt;country = &#x27;UK&#x27;&lt;&#x2F;code&gt; are correlated. It multiplies their selectivities independently, potentially underestimating by orders of magnitude.&lt;&#x2F;p&gt;
&lt;p&gt;Extended statistics solve this for specific column combinations:&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 STATISTICS&lt;&#x2F;span&gt;&lt;span&gt; orders_status_date (dependencies, ndistinct, mcv)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    ON status&lt;&#x2F;span&gt;&lt;span&gt;, created_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; orders;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ANALYZE orders;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This tells ANALYZE to compute functional dependencies, combined distinct counts, and combined MCVs between the columns. The planner can then use these to avoid the independence assumption for queries filtering on both columns.&lt;&#x2F;p&gt;
&lt;p&gt;The three types of extended statistics serve different purposes:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;dependencies&lt;&#x2F;strong&gt; capture functional dependencies between columns. Helps when knowing one column&#x27;s value determines or narrows another&#x27;s (e.g. &lt;code&gt;zip_code&lt;&#x2F;code&gt; largely determines &lt;code&gt;city&lt;&#x2F;code&gt;).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ndistinct&lt;&#x2F;strong&gt; tracks the number of distinct value combinations across columns. Helps with &lt;code&gt;GROUP BY&lt;&#x2F;code&gt; on multiple columns where the planner would otherwise multiply distinct counts independently.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;mcv&lt;&#x2F;strong&gt; builds a combined most-common-values list for column tuples. The most powerful but most expensive option. Helps with multi-column WHERE conditions on correlated values.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;You can create extended statistics with any combination of these types. Start with &lt;code&gt;dependencies&lt;&#x2F;code&gt; as it&#x27;s cheapest, add &lt;code&gt;mcv&lt;&#x2F;code&gt; when multi-column filter estimates are consistently wrong.&lt;&#x2F;p&gt;
&lt;p&gt;Extended statistics are worth creating when you see &lt;code&gt;EXPLAIN&lt;&#x2F;code&gt; estimates that are consistently wrong on multi-column filters, and the columns are logically correlated. They are computed during step 5 of the ANALYZE process and stored in &lt;code&gt;pg_statistic_ext_data&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;diagnosing-bad-estimates&quot;&gt;Diagnosing bad estimates&lt;a class=&quot;zola-anchor&quot; href=&quot;#diagnosing-bad-estimates&quot; aria-label=&quot;Anchor link for: diagnosing-bad-estimates&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;When a query is slow, the first question should always be: did the planner estimate correctly? Compare the estimate to reality with &lt;code&gt;EXPLAIN ANALYZE&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Estimate off by handful of rows means statistics are fine. But when you see estimates off by 10x or more, that&#x27;s where planning goes wrong. A nested loop that looks cheap for 100 rows becomes a disaster at 10,000.&lt;&#x2F;p&gt;
&lt;p&gt;The statistics tell you what the planner believed, and comparing that with reality tells you what to do next. Either run &lt;code&gt;ANALYZE&lt;&#x2F;code&gt;, consider tuning the statistics target for a specific column, or creating extended statistics if multiple columns are involved.&lt;&#x2F;p&gt;
&lt;p&gt;The planner is only as good as what it reads from the catalog. When estimates go wrong, don&#x27;t blame the planner. Check the data it&#x27;s working with.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Inside PostgreSQL&#x27;s 8KB Page</title>
        <published>2026-02-19T21:52:00+00:00</published>
        <updated>2026-02-19T21:52:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/inside-the-8kb-page/"/>
        <id>https://boringsql.com/posts/inside-the-8kb-page/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/inside-the-8kb-page/">&lt;p&gt;If you read previous &lt;a href=&quot;&#x2F;posts&#x2F;introduction-to-buffers&#x2F;&quot;&gt;post about buffers&lt;&#x2F;a&gt;, you already know PostgreSQL might not necessarily care about your rows. You might be inserting a user profile, or retrieving payment details, but all that Postgres works with are blocks of data. 8KB blocks, to be precise. You want to retrieve one tiny row? PostgreSQL hauls an entire 8,192-byte page off the disk just to give it to you. You update a single boolean flag? Same thing. The 8KB page is THE atomic unit of I&#x2F;O.&lt;&#x2F;p&gt;
&lt;p&gt;But knowing those pages exist isn&#x27;t enough. To understand why the database behaves the way it does, you need to understand how it works. Every time you execute &lt;code&gt;INSERT&lt;&#x2F;code&gt;, PostgreSQL needs to figure out how to fit it into one of those 8,192-byte pages.&lt;&#x2F;p&gt;
&lt;p&gt;The buffer pool caches them, Write-Ahead Log (WAL) protects them, and VACUUM cleans them. The deep dive into the PostgreSQL storage internals starts by understanding what happens inside those 8KB pages. Pages that are used by PostgreSQL to organize all data - tables, indexes, sequences, TOAST relations.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-8kb&quot;&gt;The 8KB&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-8kb&quot; aria-label=&quot;Anchor link for: the-8kb&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;div class=&quot;sidenote&quot;&gt;In case of Oracle, the default block size is set at database creation (&lt;code&gt;DB_BLOCK_SIZE&lt;&#x2F;code&gt;), though tablespaces with non-standard block sizes can be created separately.&lt;&#x2F;div&gt;Before looking inside we actually need to discuss why 8KB in first place? The answer might be surprising - it&#x27;s a setting that survived for over 40 years and nobody found a good reason to change it. Plus PostgreSQL isn&#x27;t the only one who thinks that way. Oracle and SQL Server use the same exact number.
&lt;p&gt;The 8KB page size can be traced down to original Berkley POSTGRES project created in mid-1980s. In those times Unix systems typically used 4KB or 8KB virtual memory pages, and disk sectors were 512 bytes. Choosing 8KB meant a single database page mapped cleanly to OS memory pages and aligned well with filesystem I&#x2F;O.&lt;&#x2F;p&gt;
&lt;p&gt;And the math still works today. Modern Linux kernels manage memory in 4KB virtual memory pages. SSDs now use 4KB physical sectors instead of 512 bytes. The default filesystem block size on ext4 and XFS is 4KB. PostgreSQL&#x27;s 8KB page still maps to two OS pages, two disk sectors, two filesystem blocks. The hardware changed underneath, but the alignment is still there.&lt;&#x2F;p&gt;
&lt;p&gt;But the choice isn&#x27;t just about hardware alignment. It&#x27;s a tradeoff between two opposing forces. Make the page size &lt;strong&gt;too small&lt;&#x2F;strong&gt; and you going to increase the overhead (page metadata being good example). Make it &lt;strong&gt;too large&lt;&#x2F;strong&gt; and you waste space and increase I&#x2F;O requirements when you need a single narrow row.&lt;&#x2F;p&gt;
&lt;p&gt;The different situation is outside OLTP world. DuckDB, designed for analytical columnar workloads, uses 256KB blocks. When you&#x27;re scanning millions of rows sequentially, the overhead-per-page penalty barely matters - you want big chunks to maximize throughput.&lt;&#x2F;p&gt;
&lt;div class=&quot;callout&quot;&gt;
Is PostgreSQL 8KB page size fixed? Technically no. PostgreSQL supports 1, 2, 4, 8, 16, or 32KB pages via &lt;code&gt;--with-blocksize&lt;&#x2F;code&gt; at compile time. Some analytical workloads with wide rows benefit from 16KB or 32KB pages. But you&#x27;ll need to rebuild everything from scratch, and unless you have a very specific reason and understand the downstream implications you &lt;strong&gt;almost certainly don&#x27;t want to. The default works.&lt;&#x2F;strong&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;You can confirm the page size on any running instance.&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;SHOW block_size;&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&gt; block_size&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 style=&quot;color: #79B8FF;&quot;&gt; 8192&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;h2 id=&quot;pageinspect&quot;&gt;pageinspect&lt;a class=&quot;zola-anchor&quot; href=&quot;#pageinspect&quot; aria-label=&quot;Anchor link for: pageinspect&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;PostgreSQL comes with an extension called &lt;code&gt;pageinspect&lt;&#x2F;code&gt; that lets you read raw page contents from SQL. It is part of the contrib modules and available on most installations. Let&#x27;s enable it and create a small test table:&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 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IF NOT EXISTS&lt;&#x2F;span&gt;&lt;span&gt; pageinspect;&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; page_demo&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;    title &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;text 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;    created_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TIMESTAMPTZ DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; CURRENT_TIMESTAMP,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    value 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;&#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; page_demo (title, created_at, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;value&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;VALUES&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: #9ECBFF;&quot;&gt;&amp;#39;Ergonomic standing desk, oak finish&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;2026-01-15 09:23:45+00&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;549&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;99&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: #9ECBFF;&quot;&gt;&amp;#39;Wireless mechanical keyboard&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;2026-01-15 10:05:12+00&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;189&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;00&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: #9ECBFF;&quot;&gt;&amp;#39;Desk lamp&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;2026-01-16 14:31:22+00&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;45&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;00&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;We have three rows. PostgreSQL has written them into a single heap page. Let&#x27;s look inside.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;reading-a-raw-page-header&quot;&gt;Reading a raw page header&lt;a class=&quot;zola-anchor&quot; href=&quot;#reading-a-raw-page-header&quot; aria-label=&quot;Anchor link for: reading-a-raw-page-header&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The &lt;code&gt;page_header&lt;&#x2F;code&gt; function takes a raw page and returns the parsed header fields:&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; page_header(get_raw_page(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;page_demo&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;&#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;-[ RECORD 1 ]---------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;lsn       | F&#x2F;1F9ED410&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;checksum  | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;flags     | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;lower     | 36&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;upper     | 7976&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;special   | 8192&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;pagesize  | 8192&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;version   | 4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;prune_xid | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Every one of those fields lives in the first 24 bytes of the page. Together they form the &lt;strong&gt;page header&lt;&#x2F;strong&gt;, and they tell PostgreSQL everything it needs to know about the page before touching any actual data.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-page-header-24-bytes-of-metadata&quot;&gt;The page header: 24 bytes of metadata&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-page-header-24-bytes-of-metadata&quot; aria-label=&quot;Anchor link for: the-page-header-24-bytes-of-metadata&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The first two fields are about safety, making sure the page survives crashes and silent corruption.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;pd_lsn (8 bytes)&lt;&#x2F;strong&gt; the Log Sequence Number of the last WAL record that modified this page. During crash recovery, PostgreSQL compares this LSN against the WAL stream. If the WAL record&#x27;s LSN is less than or equal to the page&#x27;s LSN, the page is already up to date and the record is skipped. If it is greater, the record must be replayed. This single field is what makes crash recovery work.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;pd_checksum (2 bytes)&lt;&#x2F;strong&gt; a checksum of the page contents. This is only active if the cluster was initialized with &lt;code&gt;initdb --data-checksums&lt;&#x2F;code&gt; (or checksums were enabled later with &lt;code&gt;pg_checksums&lt;&#x2F;code&gt;). When enabled, PostgreSQL verifies the checksum every time it reads a page from disk. A mismatch means silent data corruption, the kind that would otherwise go undetected until your data is wrong in ways nobody can explain.&lt;&#x2F;p&gt;
&lt;p&gt;Are checksums enabled on your cluster? Before PostgreSQL 17, they were off by default. Check with &lt;code&gt;SHOW data_checksums;&lt;&#x2F;code&gt;. If you see &lt;code&gt;off&lt;&#x2F;code&gt; on a production database, that&#x27;s worth fixing. You can enable them retroactively with &lt;code&gt;pg_checksums&lt;&#x2F;code&gt;, though it requires a full shutdown.&lt;&#x2F;p&gt;
&lt;div class=&quot;sidenote&quot;&gt;&lt;strong&gt;Magic of &lt;code&gt;PD_ALL_VISIBLE&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;&lt;p&gt;
Indexes know what your data is, but not who is allowed to see it (in case a row was recently deleted or updated). Postgres has to do extra work to double-check the main table (the &quot;heap&quot;) to ensure a row is visible.&lt;&#x2F;p&gt;
&lt;p&gt;When a page is marked PD_ALL_VISIBLE, it guarantees every row on that page is old enough to be seen by everyone. This lets Postgres skip the expensive heap check entirely and serve data straight from the index. A speed boost you may know as an Index-Only Scan.
&lt;&#x2F;p&gt;
&lt;&#x2F;strong&gt;&lt;&#x2F;div&gt;
&lt;p&gt;The next fields are the page&#x27;s spatial ma. They tell PostgreSQL where things are and where there&#x27;s room for more.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;pd_flags (2 bytes)&lt;&#x2F;strong&gt; bit flags that describe the page state. The important ones are: &lt;code&gt;PD_HAS_FREE_LINES&lt;&#x2F;code&gt; (are there any unused line pointers?), &lt;code&gt;PD_PAGE_FULL&lt;&#x2F;code&gt; (not enough free space for new tuple) and &lt;code&gt;PD_ALL_VISIBLE&lt;&#x2F;code&gt; (all tuples on page are visible to everyone).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;pd_lower&lt;&#x2F;strong&gt; and &lt;strong&gt;pd_upper&lt;&#x2F;strong&gt; (2 bytes each) define the free space gap. &lt;code&gt;pd_lower&lt;&#x2F;code&gt; marks where the line pointer array ends -- in our output it is 36, that is 24 bytes of header plus 3 line pointers at 4 bytes each: 24 + (3 x 4) = 36. &lt;code&gt;pd_upper&lt;&#x2F;code&gt; marks where tuple data begins -- our value is 7976, meaning the three tuples together occupy bytes 7976 through 8191. Everything between these two offsets is free space. As you insert rows, these two values creep toward each other until there&#x27;s no room left.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;pd_special (2 bytes)&lt;&#x2F;strong&gt; the byte offset to the &quot;special space&quot; at the end of the page. For heap (table) pages, this equals the page size (8192), meaning there is no special space. For index pages, this area contains index-specific metadata.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;pd_pagesize_version (2 bytes)&lt;&#x2F;strong&gt; encodes both the page size and the layout version number. The version is currently 4 for all modern PostgreSQL releases.&lt;&#x2F;p&gt;
&lt;p&gt;Finally, one field dedicated to cleanup.&lt;&#x2F;p&gt;
&lt;div class=&quot;sidenote&quot;&gt;&lt;strong&gt;The Mini-Cleanup (Page-Level Pruning)&lt;&#x2F;strong&gt;
&lt;p&gt;Instead of waiting for a heavy VACUUM, Postgres can do garbage collection on the fly. When a normal query reads a page, it checks `pd_prune_xid`. If that transaction ID is older than all currently running transactions, the query instantly reclaims the dead space itself.&lt;&#x2F;p&gt;
&lt;p&gt;This is one of the cases where SELECT might modify data pages.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;&lt;strong&gt;pd_prune_xid (4 bytes)&lt;&#x2F;strong&gt; the oldest transaction ID whose dead tuples have not yet been pruned from this page. When a new transaction accesses the page and finds that &lt;code&gt;prune_xid&lt;&#x2F;code&gt; is older than the global horizon, it triggers page-level pruning. A lightweight cleanup that reclaims space without a full VACUUM.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-is-a-line-pointer&quot;&gt;What is a line pointer?&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-is-a-line-pointer&quot; aria-label=&quot;Anchor link for: what-is-a-line-pointer&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;If you&#x27;re paying attention, we just mentioned line pointers in &lt;code&gt;pd_lower&lt;&#x2F;code&gt; description. If the page header discussed above is the general metadata, the &lt;strong&gt;line pointers&lt;&#x2F;strong&gt; (internally represented as &lt;code&gt;ItemIdData&lt;&#x2F;code&gt;) are the page table of contents.&lt;&#x2F;p&gt;
&lt;p&gt;Every time you insert a row, PostgreSQL doesn&#x27;t just drop the raw data into the page. It actually splits the job into two steps.&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;It puts the bulky, unpredictable row data (the tuple) at the very bottom of the page (more on this in the next section).&lt;&#x2F;li&gt;
&lt;li&gt;It adds a tiny, fixed 4-byte line pointer right after the page header.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;This pointer acts as a direct map. It holds exact byte offset and length of the tuple it points to. It allows PostgreSQL to only look at the offset and read the required number of bytes to get the tuple.&lt;&#x2F;p&gt;
&lt;p&gt;The line pointer array starts immediately after the 24-byte header and grows downward. Let&#x27;s look at ours.&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; lp, lp_off, lp_flags, lp_len&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; heap_page_items(get_raw_page(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;page_demo&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;&#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; lp | lp_off | lp_flags | lp_len&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;  1 |   8112 |        1 |     79&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  2 |   8032 |        1 |     77&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  3 |   7976 |        1 |     53&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Each line pointer is 4 bytes and contains three pieces of information:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;lp&lt;&#x2F;strong&gt; the ordinal position in the array (1-based). This, combined with the page number, forms the &lt;strong&gt;ctid&lt;&#x2F;strong&gt; - the physical address of a tuple. Row 1 on page 0 has ctid &lt;code&gt;(0,1)&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;lp_off&lt;&#x2F;strong&gt; the byte offset within the page where the actual tuple data begins. Notice the offsets decrease: 8112, 8032, 7976. Tuples are packed from the bottom up.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;div class=&quot;sidenote&quot;&gt;
Line pointers are the reason PostgreSQL can move tuples around within a page (during defragmentation) or perform &lt;strong&gt;HOT (Heap-Only Tuple) updates&lt;&#x2F;strong&gt; without invalidating index entries. An index stores a ctid like &lt;code&gt;(0, 2)&lt;&#x2F;code&gt;, which means &quot;page 0, line pointer 2&quot;. Because the index points to the pointer rather than the physical byte offset, Postgres can shuffle data or redirect pointers under the hood while the index reference remains perfectly valid.
&lt;&#x2F;div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;lp_flags&lt;&#x2F;strong&gt; the state of the pointer. The values are: 0 = &lt;code&gt;LP_UNUSED&lt;&#x2F;code&gt; (available for reuse), 1 = &lt;code&gt;LP_NORMAL&lt;&#x2F;code&gt; (points to a live tuple), 2 = &lt;code&gt;LP_REDIRECT&lt;&#x2F;code&gt; (points to another line pointer, used after HOT pruning), 3 = &lt;code&gt;LP_DEAD&lt;&#x2F;code&gt; (the tuple has been determined dead but the pointer persists until cleanup).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;lp_len&lt;&#x2F;strong&gt; the total length of the tuple in bytes, including the 23-byte tuple header, null bitmap, alignment padding, and actual column data.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;the-anatomy-of-a-page&quot;&gt;The anatomy of a page&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-anatomy-of-a-page&quot; aria-label=&quot;Anchor link for: the-anatomy-of-a-page&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Here is how all these pieces fit together inside the 8KB block.&lt;&#x2F;p&gt;
&lt;img class=&quot;only-light&quot; src=&quot;&#x2F;images&#x2F;posts&#x2F;page-inspector-light.png&quot; alt=&quot;PostgreSQL page inspector&quot;&gt;
&lt;img class=&quot;only-dark&quot; src=&quot;&#x2F;images&#x2F;posts&#x2F;page-inspector-dark.png&quot; alt=&quot;PostgreSQL page inspector&quot;&gt;
&lt;p&gt;We already mentioned it above, but if you review the image carefully you can confirm the important insight. While line pointers are allocated downward from the header (pd_lower increases) the tuple data is stored upwards from the bottom of the page (pd_upper decreases). Free space is the gap between them.&lt;&#x2F;p&gt;
&lt;p&gt;This is called &lt;strong&gt;Slotted page layout&lt;&#x2F;strong&gt; and it&#x27;s easy way how to prevent unnecessary data fragmentation. By storing predictable line pointers at top, they don&#x27;t mix together with bulky and unpredicable tuples.&lt;&#x2F;p&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:6px; border-radius:2px; background:var(--viz-page-header);&quot;&gt;&lt;&#x2F;div&gt;
        &lt;div style=&quot;height:8px; border-radius:2px; background:var(--viz-page-lp);&quot;&gt;&lt;&#x2F;div&gt;
        &lt;div style=&quot;height:20px; border-radius:2px; background:var(--color-tertiary); background-image:repeating-linear-gradient(45deg,transparent,transparent 2px,var(--color-border) 2px,var(--color-border) 3px);&quot;&gt;&lt;&#x2F;div&gt;
        &lt;div style=&quot;height:14px; border-radius:2px; background:var(--viz-page-tuple);&quot;&gt;&lt;&#x2F;div&gt;
    &lt;&#x2F;div&gt;
    &lt;div class=&quot;visualizer-banner__content&quot;&gt;
        &lt;strong&gt;Interactive Page Visualizer&lt;&#x2F;strong&gt;
        &lt;p&gt;Watch the opposing growth directions in action. Insert rows and click regions for byte-level details.&lt;&#x2F;p&gt;
        &lt;a href=&quot;&#x2F;visualizers&#x2F;8kb-page&#x2F;&quot; class=&quot;visualizer-banner__button&quot;&gt;Open Visualizer&lt;&#x2F;a&gt;
    &lt;&#x2F;div&gt;
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;free-space-the-gap-in-the-middle&quot;&gt;Free space: the gap in the middle&lt;a class=&quot;zola-anchor&quot; href=&quot;#free-space-the-gap-in-the-middle&quot; aria-label=&quot;Anchor link for: free-space-the-gap-in-the-middle&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The free space on a page is the region between pd_lower and pd_upper. For our page:&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; lower, upper, upper &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span&gt; lower &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; free_space&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; page_header(get_raw_page(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;page_demo&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;&#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; lower | upper | free_space&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;    36 |  7976 |       7940&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 only three small rows, we have 7,940 bytes of free space. Almost the entire page is still available. Let&#x27;s see what happens when we add more 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;INSERT INTO&lt;&#x2F;span&gt;&lt;span&gt; page_demo (title, created_at, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;value&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;SELECT&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;Generated item &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span&gt; i, &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;2026-01-15 00:00:00+00&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz +&lt;&#x2F;span&gt;&lt;span&gt; (i &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; hours&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)::interval, &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    (i &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 11&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;11&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 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;10&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; i;&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;SELECT&lt;&#x2F;span&gt;&lt;span&gt; lower, upper, upper &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span&gt; lower &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; free_space&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; page_header(get_raw_page(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;page_demo&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;&#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; lower | upper | free_space&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;    76 |  7336 |       7260&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;You can observe&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pd_lower&lt;&#x2F;code&gt; moved from 36 to 76. Why? With 10 new line pointers at 4 bytes each we moved by 40 bytes.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pd_upper&lt;&#x2F;code&gt; dropped from 7,976 to 7,336. The 10 new tuples consumed 640 bytes of the data space.&lt;&#x2F;li&gt;
&lt;li&gt;And the free space shrank to 7,260 bytes.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;how-fast-does-a-page-fill-up&quot;&gt;How fast does a page fill up?&lt;a class=&quot;zola-anchor&quot; href=&quot;#how-fast-does-a-page-fill-up&quot; aria-label=&quot;Anchor link for: how-fast-does-a-page-fill-up&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;If we going to think about the page capacity, let&#x27;s consider each row in this page costs:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;4 bytes&lt;&#x2F;strong&gt; for the line pointer&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;23 bytes&lt;&#x2F;strong&gt; for the tuple header (transaction metadata, null bitmap offset, info mask)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Alignment padding&lt;&#x2F;strong&gt; to the nearest 8-byte boundary after the header (1 byte of padding, bringing the header to 24 bytes)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Actual column data&lt;&#x2F;strong&gt;: 4 bytes for the integer, variable bytes for the text, variable bytes for the numeric&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;For our new schema, each tuple is a bit larger than before. We saw earlier that our first 3 tuples took up exactly 216 bytes, which averages exactly 72 bytes per tuple. Add the 4-byte line pointer, and each row costs about 76 bytes of page space.&lt;&#x2F;p&gt;
&lt;p&gt;With 8,192 bytes in a page minus the 24-byte header, we have 8,168 bytes of usable space. At roughly 76 bytes per row, we can theoretically fit approximately 107 rows on a single page.&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s test it. We already have 13 rows (3 original plus 10 generated). Let&#x27;s insert more and check:&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;INSERT INTO&lt;&#x2F;span&gt;&lt;span&gt; page_demo (title, created_at, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;value&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;SELECT&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;Generated item &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span&gt; i,&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;2026-01-15 00:00:00+00&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz +&lt;&#x2F;span&gt;&lt;span&gt; (i &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; hours&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)::interval,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    (i &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 11&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;11&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 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;11&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;150&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; i;&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;SELECT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; count&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;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; tuples_on_page_0&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; heap_page_items(get_raw_page(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;page_demo&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;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Total of 119 tuples fit on page 0 before PostgreSQL had to start using page 1. A bit more than our estimate of 107 -- the shorter generated titles take less space than &quot;Ergonomic standing desk, oak finish&quot;.&lt;&#x2F;p&gt;
&lt;p&gt;The exact number always depends on column values, but this gives you a practical sense of capacity. Because this schema includes a variable-length text column, the row size flexes, but we still hit a very predictable ceiling right around 110-120 rows per 8KB block.&lt;&#x2F;p&gt;
&lt;div class=&quot;callout&quot;&gt;
If you want to estimate how many pages a table will need, the rough formula is: &lt;code&gt;rows * average_row_size &#x2F; 8192&lt;&#x2F;code&gt;. But remember that &quot;average_row_size&quot; includes the 23-byte tuple header, alignment padding, and the 4-byte line pointer.
&lt;p&gt;PostgreSQL&#x27;s &lt;code&gt;pg_column_size()&lt;&#x2F;code&gt; function can help you measure actual row sizes.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;spanning-multiple-pages&quot;&gt;Spanning multiple pages&lt;a class=&quot;zola-anchor&quot; href=&quot;#spanning-multiple-pages&quot; aria-label=&quot;Anchor link for: spanning-multiple-pages&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;With 160 rows in our table, we have started storing the tuples into a second page. Let&#x27;s verify using the system catalog:&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; 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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&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;page_demo&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;div class=&quot;sidenote&quot;&gt;The &lt;code&gt;relpages&lt;&#x2F;code&gt; and &lt;code&gt;reltuples&lt;&#x2F;code&gt; values are estimates updated by ANALYZE and autovacuum. After bulk inserts, run &lt;code&gt;ANALYZE page_demo;&lt;&#x2F;code&gt; to refresh them.&lt;&#x2F;div&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;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;        2 |       160&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;Two pages. Let&#x27;s insert substantially more data to see a real multi-page table:&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;INSERT INTO&lt;&#x2F;span&gt;&lt;span&gt; page_demo (title, created_at, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;value&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;SELECT&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;Generated row &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span&gt; i, &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    CURRENT_TIMESTAMP &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;+&lt;&#x2F;span&gt;&lt;span&gt; (i &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; minutes&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)::interval,&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; 1000&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 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;500&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; i;&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&gt;ANALYZE page_demo;&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;SELECT&lt;&#x2F;span&gt;&lt;span&gt; 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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&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;page_demo&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; 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;        6 |       660&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;Six pages now. Each page is an independent 8KB block with its own header. Let&#x27;s confirm by reading the headers of the first two:&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 style=&quot;color: #79B8FF;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS page&lt;&#x2F;span&gt;&lt;span&gt;, lower, upper, upper &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span&gt; lower &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; free_space&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; page_header(get_raw_page(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;page_demo&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;UNION ALL&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 style=&quot;color: #79B8FF;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span&gt;, lower, upper, upper &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span&gt; lower&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; page_header(get_raw_page(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;page_demo&amp;#39;&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;&#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; page | lower | upper | free_space&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;    0 |   500 |   552 |         52&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    1 |   504 |   512 |          8&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;Both pages are nearly full, with only 52&#x2F;8 bytes of free space remaining. Which is too little for another row to fit there.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-space-that-isn-t-there&quot;&gt;The space that isn&#x27;t there&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-space-that-isn-t-there&quot; aria-label=&quot;Anchor link for: the-space-that-isn-t-there&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;You may have noticed that &lt;code&gt;pd_special&lt;&#x2F;code&gt; equals 8192 for our heap pages. The same size as the entire page size. I.e. there&#x27;s no special space allocated.&lt;&#x2F;p&gt;
&lt;p&gt;This applies to the heap pages. But not all pages are heap pages. Index pages use the special space at the end of the page to store index-specific metadata. For a B-tree index, it includes pointers to sibling pages (for range scans), the tree level, and flags about the page type (leaf, internal, root, deleted).&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s peek at the primary key index that was automatically created for our table:&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 type&lt;&#x2F;span&gt;&lt;span&gt;, live_items, dead_items, avg_item_size,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       page_size, free_size&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; bt_page_stats(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;page_demo_pkey&amp;#39;&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;&#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; type | live_items | dead_items | avg_item_size | page_size | free_size&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; l    |        367 |          0 |            16 |      8192 |       808&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;The &lt;code&gt;type&lt;&#x2F;code&gt; is &lt;code&gt;l&lt;&#x2F;code&gt; for leaf page. And if we check its page header:&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; special &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; page_header(get_raw_page(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;page_demo_pkey&amp;#39;&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;&#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; special&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;    8176&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;The special space starts at byte 8176, giving us 16 bytes (8192 - 8176) of B-tree metadata at the end of the page. Heap pages have none; index pages rely on it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;putting-it-all-together&quot;&gt;Putting it all together&lt;a class=&quot;zola-anchor&quot; href=&quot;#putting-it-all-together&quot; aria-label=&quot;Anchor link for: putting-it-all-together&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Let&#x27;s close with a complete view of our page. We know the header, the line pointers, the free space, and the tuple data regions. Here is a summary query that shows all of it at once:&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;header&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;       AS&lt;&#x2F;span&gt;&lt;span&gt; region,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    0&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;              AS&lt;&#x2F;span&gt;&lt;span&gt; start_byte,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    23&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;             AS&lt;&#x2F;span&gt;&lt;span&gt; end_byte,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    24&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;             AS&lt;&#x2F;span&gt;&lt;span&gt; size_bytes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;UNION ALL&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 style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;line pointers&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: #79B8FF;&quot;&gt;    24&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;    lower &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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    lower &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 24&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; page_header(get_raw_page(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;page_demo&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;UNION ALL&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 style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;free space&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;    lower,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    upper &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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    upper &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span&gt; lower&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; page_header(get_raw_page(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;page_demo&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;UNION ALL&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 style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;tuple data&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;    upper,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    special &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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    special &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span&gt; upper&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; page_header(get_raw_page(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;page_demo&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;UNION ALL&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 style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;special space&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;    special,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    8191&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;    8192&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; -&lt;&#x2F;span&gt;&lt;span&gt; special&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; page_header(get_raw_page(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;page_demo&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;&#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;    region     | start_byte | end_byte | size_bytes&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; header        |          0 |       23 |         24&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; line pointers |         24 |      499 |        476&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; free space    |        500 |      551 |         52&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; tuple data    |        552 |     8191 |       7640&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; special space |       8192 |     8191 |          0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(5 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;476 bytes of line pointers means 119 entries (476 &#x2F; 4). 7,640 bytes of tuple data. 52 bytes of free space. And zero bytes of special space (it&#x27;s heap page).&lt;&#x2F;p&gt;
&lt;p&gt;That is the entire PostgreSQL 8KB page. Twenty-four bytes of header tell PostgreSQL where everything is. Line pointers provide table of contents.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Reading Buffer statistics in EXPLAIN output</title>
        <published>2026-02-06T15:52:00+00:00</published>
        <updated>2026-02-06T15:52:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/explain-buffers/"/>
        <id>https://boringsql.com/posts/explain-buffers/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/explain-buffers/">&lt;p&gt;In the article about &lt;a href=&quot;&#x2F;posts&#x2F;introduction-to-buffers&#x2F;&quot;&gt;Buffers in PostgreSQL&lt;&#x2F;a&gt; we kept adding &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;&#x2F;code&gt; to every query without giving much thought to the output. Time to fix that. PostgreSQL breaks down buffer usage for each plan node, and once you learn to read those numbers, you&#x27;ll know exactly where your query spent time waiting for I&#x2F;O - and where it didn&#x27;t have to. That&#x27;s about as fundamental as it gets when diagnosing performance problems.&lt;&#x2F;p&gt;
&lt;div class=&quot;callout&quot;&gt;
&lt;strong&gt;PostgreSQL 18: BUFFERS by Default&lt;&#x2F;strong&gt;&lt;br&gt;
Starting with PostgreSQL 18, &lt;code&gt;EXPLAIN ANALYZE&lt;&#x2F;code&gt; automatically includes buffer statistics - you no longer need to explicitly add &lt;code&gt;BUFFERS&lt;&#x2F;code&gt;. The examples below use the explicit syntax for compatibility with older versions, but on PG18+ a simple &lt;code&gt;EXPLAIN ANALYZE&lt;&#x2F;code&gt; gives you the same information.
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;a-complete-example&quot;&gt;A complete example&lt;a class=&quot;zola-anchor&quot; href=&quot;#a-complete-example&quot; aria-label=&quot;Anchor link for: a-complete-example&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;For this article we will use following schema and seeded 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;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; customers&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 style=&quot;color: #F97583;&quot;&gt;    name text NOT NULL&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;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; 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 REFERENCES&lt;&#x2F;span&gt;&lt;span&gt; customers(id),&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;    note &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&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; customers (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;name&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;SELECT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Customer &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span&gt; i&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;2000&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; i;&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;-- seed data: ~100,000 orders spread across 2022-2025&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; orders (customer_id, amount, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;status&lt;&#x2F;span&gt;&lt;span&gt;, note, 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; 1999&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; 500&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: #F97583;&quot;&gt;    CASE WHEN&lt;&#x2F;span&gt;&lt;span&gt; random()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;lt;&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;3&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; THEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Some note text here for padding&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ELSE NULL END&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;2022-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; 1095&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 style=&quot;color: #6A737D;&quot;&gt;  -- ~3 years of data&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;100000&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;-- make sure stats are up to date&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ANALYZE customers;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ANALYZE orders;&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;-- we are going to skip indexes on purpose&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;-- and fire sample query&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 style=&quot;color: #79B8FF;&quot;&gt; count&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: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; customers;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Let&#x27;s start with a random query&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 (ANALYZE, BUFFERS)&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; o.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;name&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; orders o&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;JOIN&lt;&#x2F;span&gt;&lt;span&gt; customers c &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;customer_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&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-01-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;p&gt;and its output.&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;                                                         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; Hash Join  (cost=58.00..2253.87 rows=33784 width=71) (actual time=0.835..26.695 rows=33239.00 loops=1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Hash Cond: (o.customer_id = c.id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Buffers: shared hit=13 read=857&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   -&amp;gt;  Seq Scan on orders o  (cost=0.00..2107.00 rows=33784 width=58) (actual time=0.108..18.106 rows=33239.00 loops=1)&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-01-01&amp;#39;::date)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         Rows Removed by Filter: 66761&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         Buffers: shared read=857&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   -&amp;gt;  Hash  (cost=33.00..33.00 rows=2000 width=17) (actual time=0.697..0.698 rows=2000.00 loops=1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         Buckets: 2048  Batches: 1  Memory Usage: 118kB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         Buffers: shared hit=13&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         -&amp;gt;  Seq Scan on customers c  (cost=0.00..33.00 rows=2000 width=17) (actual time=0.007..0.231 rows=2000.00 loops=1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;               Buffers: shared hit=13&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Planning:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Buffers: shared hit=130 read=29 dirtied=3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Planning Time: 1.585 ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Execution Time: 28.067 ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(16 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;As that&#x27;s quite a bit of information, let&#x27;s break it down by individual categories.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shared-buffers-hit-read-dirtied-written&quot;&gt;Shared buffers: hit, read, dirtied &amp;amp; written&lt;a class=&quot;zola-anchor&quot; href=&quot;#shared-buffers-hit-read-dirtied-written&quot; aria-label=&quot;Anchor link for: shared-buffers-hit-read-dirtied-written&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;As described in &lt;a href=&quot;&#x2F;posts&#x2F;introduction-to-buffers&#x2F;&quot;&gt;previous article&lt;&#x2F;a&gt; these are most common buffers statistics you will see.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;shared hit&lt;&#x2F;strong&gt; is number of pages found in shared buffers (i.e. cached). This is fast path where no disk I&#x2F;O is required. Higher is better for performance.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;shared read&lt;&#x2F;strong&gt; identifies number of pages not in shared buffers, fetched from disk (or OS cache), and each one of them adds potential I&#x2F;O latency.&lt;&#x2F;p&gt;
&lt;div class=&quot;sidenote&quot;&gt;If you see a SELECT that shows `dirtied` pages. It&#x27;s not a bug. PostgreSQL sets hint bits and prunes HOT chains during reads - the first backend to read a page after writes will dirty it. Normal behavior, not a problem.&lt;&#x2F;div&gt;**shared dirtied** has number of pages modified by this query. The query changed the data that was already cached (in the buffer pool) and those pages will eventually need to be written to the disk.
&lt;p&gt;&lt;strong&gt;shared written&lt;&#x2F;strong&gt; is a number of pages written to disk during query execution. To remind us, this happens when query needs buffer space but needs to evict dirty pages synchronously. If you see this repeatedly during a SELECT it might be a warning sign - your background writer is not keeping up as it should.&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s have a look at our query&#x27;s top-level buffer 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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Buffers: shared hit=13 read=857&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Only 13 pages were cached in shared buffers, while 857 had to be fetched from disk (or OS cache). No pages were dirtied or written - expected for a pure SELECT with no side effects.&lt;&#x2F;p&gt;
&lt;p&gt;But where did those 13 hits come from? The breakdown by node tells us:&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;-&amp;gt;  Seq Scan on orders o&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      Buffers: shared read=857&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&amp;gt;  Seq Scan on customers c&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      Buffers: shared hit=13&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The customers table (in this case small - 2,000 rows, 13 pages) was fully cached - likely accessed frequently or as it&#x27;s in our case accessed recently. The orders table (100,000 rows, 857 pages) had zero hits - every single page required I&#x2F;O. This is typical after a restart or when scanning a table that doesn&#x27;t fit comfortably in shared buffers.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;interpreting-the-ratio&quot;&gt;Interpreting the ratio&lt;a class=&quot;zola-anchor&quot; href=&quot;#interpreting-the-ratio&quot; aria-label=&quot;Anchor link for: interpreting-the-ratio&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;In the context of this article we&#x27;re going to consider ratio between shared hit and total buffers processed. Is there a perfect ratio you should strive for? As we will demonstrate there&#x27;s no such universal value.&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s calculate it for our query:&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;hit_ratio = shared hit &#x2F; (shared hit + shared read)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          = 13 &#x2F; (13 + 857)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          = 1.5%&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;div class=&quot;sidenote&quot;&gt;In an OLTP workload, the same small set of rows gets accessed over and over - fetch a customer by ID, look up an order by reference number, check inventory for a product. The working set is a small fraction of the total database. These queries touch a handful of pages each, and those pages stay hot in shared buffers because they keep getting requested. A well-tuned OLTP system naturally converges toward high hit ratios - not because someone set a target, but because the access pattern keeps the relevant data cached.&lt;&#x2F;div&gt;Honestly, that looks terrible. If this were the ratio of most of your OLTP queries you can easily say - there&#x27;s a problem. But this was run on a freshly loaded dataset with cold caches - every page of the orders table had to be fetched for the first time. Run the same query again and you&#x27;ll likely see most of those 857 reads become hits as shared buffers and the OS page cache warm up. On a test environment (where nothing else runs you will most likely hit the 100%).
&lt;p&gt;What matters is the hit ratio &lt;em&gt;per query&lt;&#x2F;em&gt;, tracked &lt;em&gt;over time&lt;&#x2F;em&gt;, compared to &lt;em&gt;its own baseline&lt;&#x2F;em&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;A reporting query scanning a large date range might consistently show 10-30% hit ratio. That&#x27;s fine - it&#x27;s expected to touch cold data.&lt;&#x2F;li&gt;
&lt;li&gt;A query serving your login page should be near 100%. If it drops to 80%, something changed - maybe the table grew, an index was rebuilt, or shared_buffers is under pressure from a new workload.&lt;&#x2F;li&gt;
&lt;li&gt;A query that ran at 95% hit ratio last week and now runs at 40% deserves investigation, regardless of whether 40% sounds &quot;good&quot; or &quot;bad&quot; in isolation.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The ratio is a diagnostic tool, not a scorecard. Use it to spot regressions, compare before-and-after when tuning, and understand where your query&#x27;s time is actually going. A low ratio paired with high execution time points you toward I&#x2F;O as the bottleneck. A high ratio with high execution time tells you to look elsewhere - maybe CPU, maybe row count, maybe a bad plan.&lt;&#x2F;p&gt;
&lt;p&gt;Context matters more than absolute numbers. Compare similar queries over time, not arbitrary benchmarks.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;local-buffers&quot;&gt;Local buffers&lt;a class=&quot;zola-anchor&quot; href=&quot;#local-buffers&quot; aria-label=&quot;Anchor link for: local-buffers&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Local buffers track I&#x2F;O for temporary tables. Unlike regular tables that live in shared buffers, temp tables use per-backend memory - each connection gets its own local buffer pool, controlled by the &lt;code&gt;temp_buffers&lt;&#x2F;code&gt; setting.&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; TEMP &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TABLE&lt;&#x2F;span&gt;&lt;span&gt; temp_large_orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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 style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;amount&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;status&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;created_at&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS&lt;&#x2F;span&gt;&lt;span&gt; customer_name&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; orders o&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;JOIN&lt;&#x2F;span&gt;&lt;span&gt; customers c &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;customer_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;amount&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; 200&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&gt;EXPLAIN (ANALYZE, BUFFERS)&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 status&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;count&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;span style=&quot;color: #79B8FF;&quot;&gt;sum&lt;&#x2F;span&gt;&lt;span&gt;(amount)&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; temp_large_orders&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GROUP BY status&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;First thing you should notice is there&#x27;s no shared buffers at all, at least in execution phase. The entire query ran against local buffers because temp tables are invisible to other backends.&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;                                                          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; HashAggregate  (cost=1281.60..1284.10 rows=200 width=72) (actual time=24.659..24.661 rows=4.00 loops=1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Group Key: status&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Batches: 1  Memory Usage: 32kB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Buffers: local hit=576&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   -&amp;gt;  Seq Scan on temp_large_orders  (cost=0.00..979.20 rows=40320 width=48) (actual time=0.009..5.965 rows=60731.00 loops=1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         Buffers: local hit=576&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Planning:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Buffers: shared hit=36 read=5&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Planning Time: 0.294 ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Execution Time: 24.708 ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(10 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The individual values that might be reported are &lt;strong&gt;local hit&#x2F;read&lt;&#x2F;strong&gt; with the same concept as shared, but for temp tables in the per-backend buffer pool.&lt;&#x2F;p&gt;
&lt;p&gt;Another value is &lt;strong&gt;local dirtied&#x2F;written&lt;&#x2F;strong&gt; representing temp table modifications. &quot;Dirtied&quot; means the query modified pages in the local buffer pool. &quot;Written&quot; means dirty pages had to be flushed to disk to make room for new ones - the same clock-sweep eviction mechanism as shared buffers, but against the local buffer pool. Unlike shared buffers, temp table writes don&#x27;t generate WAL and aren&#x27;t subject to checkpointing.&lt;&#x2F;p&gt;
&lt;p&gt;In practice, &lt;code&gt;local written&lt;&#x2F;code&gt; is rare to see - PostgreSQL handles temp table overflow efficiently enough that you&#x27;re unlikely to encounter it unless your &lt;code&gt;temp_buffers&lt;&#x2F;code&gt; is severely undersized relative to your temp table workload.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;temp-buffers-when-work-mem-isn-t-enough&quot;&gt;Temp buffers: when work_mem isn&#x27;t enough&lt;a class=&quot;zola-anchor&quot; href=&quot;#temp-buffers-when-work-mem-isn-t-enough&quot; aria-label=&quot;Anchor link for: temp-buffers-when-work-mem-isn-t-enough&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;While local buffers are not that often considered a problem, or visible, temp buffers track cases where operations spill from memory to disk for sorts, hashes, and other operations that exceed current &lt;code&gt;work_mem&lt;&#x2F;code&gt; settings.&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;SET&lt;&#x2F;span&gt;&lt;span&gt; work_mem &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;256kB&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;EXPLAIN (ANALYZE, BUFFERS)&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 style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;amount&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;status&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;created_at&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;name&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; orders o&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;JOIN&lt;&#x2F;span&gt;&lt;span&gt; customers c &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;customer_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ORDER BY&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;amount&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; DESC&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;We explictely forced lower &lt;code&gt;work_mem&lt;&#x2F;code&gt; to see the impact.&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;                                                              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; Sort  (cost=38374.70..38874.70 rows=200000 width=36) (actual time=109.345..120.574 rows=200000.00 loops=1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Sort Key: o.amount DESC&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Sort Method: external merge  Disk: 9736kB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Buffers: shared hit=1738, temp read=3636 written=3722&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   -&amp;gt;  Hash Join  (cost=116.00..4353.56 rows=200000 width=36) (actual time=1.597..34.857 rows=200000.00 loops=1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         Hash Cond: (o.customer_id = c.id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         Buffers: shared hit=1738&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         -&amp;gt;  Seq Scan on orders o  (cost=0.00..3712.00 rows=200000 width=27) (actual time=0.016..6.973 rows=200000.00 loops=1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;               Buffers: shared hit=1712&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         -&amp;gt;  Hash  (cost=66.00..66.00 rows=4000 width=17) (actual time=1.568..1.569 rows=4000.00 loops=1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;               Buckets: 4096  Batches: 1  Memory Usage: 235kB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;               Buffers: shared hit=26&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;               -&amp;gt;  Seq Scan on customers c  (cost=0.00..66.00 rows=4000 width=17) (actual time=0.012..0.629 rows=4000.00 loops=1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                     Buffers: shared hit=26&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Planning:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Buffers: shared hit=15&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Planning Time: 1.184 ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Execution Time: 123.932 ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;There you can see the &lt;strong&gt;temp read&#x2F;written&lt;&#x2F;strong&gt; with number of pages read from and written to temporary files on disk. This indicates the operation couldn&#x27;t fit in memory.&lt;&#x2F;p&gt;
&lt;div class=&quot;callout&quot;&gt;
&lt;strong&gt;Naming Confusion Alert&lt;&#x2F;strong&gt;&lt;&#x2F;br&gt;
&lt;p&gt;&lt;code&gt;temp read&#x2F;written&lt;&#x2F;code&gt; in EXPLAIN has &lt;strong&gt;nothing&lt;&#x2F;strong&gt; to do with the &lt;code&gt;temp_buffers&lt;&#x2F;code&gt; parameter.&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;temp_buffers&lt;&#x2F;code&gt; = memory for temporary tables (&lt;code&gt;CREATE TEMP TABLE&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;temp read&#x2F;written&lt;&#x2F;code&gt; = disk spill from sorts&#x2F;hashes (governed by &lt;code&gt;work_mem&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;The &lt;code&gt;Sort Method: external merge  Disk: 9736kB&lt;&#x2F;code&gt; confirms it - sorting 200,000 rows with only 256kB of &lt;code&gt;work_mem&lt;&#x2F;code&gt; forced PostgreSQL to spill ~9.7MB to temporary files on disk. The &lt;code&gt;temp written=3722&lt;&#x2F;code&gt; happened during the sort phase as pages were flushed out, and &lt;code&gt;temp read=3636&lt;&#x2F;code&gt; happened during the merge phase as PostgreSQL read them back to produce the final sorted result.&lt;&#x2F;p&gt;
&lt;p&gt;Notice something else: the Hash Join and everything below it shows only &lt;code&gt;shared hit=1738&lt;&#x2F;code&gt; with no temp buffers at all. The hash table for 4,000 customers fit comfortably in 235kB of memory. The temp spill is isolated to the Sort node - buffer stats always attribute I&#x2F;O to the node that caused it.&lt;&#x2F;p&gt;
&lt;p&gt;Try bumping &lt;code&gt;work_mem&lt;&#x2F;code&gt; to something reasonable and the spill disappears:&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;SET&lt;&#x2F;span&gt;&lt;span&gt; work_mem &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;16MB&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;You should see no &lt;code&gt;temp&lt;&#x2F;code&gt; buffers at all. The sort completed in memory, execution time dropped, and the only I&#x2F;O was reading the actual table data.&lt;&#x2F;p&gt;
&lt;p&gt;To reduce temp file usage you can:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Increase &lt;code&gt;work_mem&lt;&#x2F;code&gt; (careful there, don&#x27;t forget it&#x27;s per-operation setting, not per-query, so a complex query with multiple sorts or hash joins allocates &lt;code&gt;work_mem&lt;&#x2F;code&gt; for each one)&lt;&#x2F;li&gt;
&lt;li&gt;Optimize the query to process fewer rows before the sort&lt;&#x2F;li&gt;
&lt;li&gt;And probably most importantly, consider adding indexes to avoid sorts entirely - an index on &lt;code&gt;orders(amount DESC)&lt;&#x2F;code&gt; would eliminate the sort node altogether&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;planning-buffers&quot;&gt;Planning buffers&lt;a class=&quot;zola-anchor&quot; href=&quot;#planning-buffers&quot; aria-label=&quot;Anchor link for: planning-buffers&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Up until now we have avoided planning buffers completely. It&#x27;s an addition that started with PostgreSQL 13, allowing you to see the buffer usage during the query planning, separate from the execution:&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; Planning:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Buffers: shared hit=36 read=5&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Why does planning need buffers? The planner reads system catalogs (&lt;code&gt;pg_class&lt;&#x2F;code&gt;, &lt;code&gt;pg_statistic&lt;&#x2F;code&gt;, &lt;code&gt;pg_index&lt;&#x2F;code&gt;, etc.) to understand table structures and statistics. Complex queries touching many tables can have a non-trivial impact on planning-time I&#x2F;O.&lt;&#x2F;p&gt;
&lt;p&gt;High &lt;code&gt;read&lt;&#x2F;code&gt; count in planning phase suggests either system catalogues aren&#x27;t cached (cold start most likely), or your query is touching many tables or columns.&lt;&#x2F;p&gt;
&lt;p&gt;If planning time is a problem, ensure system catalogs stay hot. On systems with many partitions, planning overhead can become significant - this is one reason partition pruning matters.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-blurry-line-between-planning-and-execution-buffers&quot;&gt;The blurry line between planning and execution buffers&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-blurry-line-between-planning-and-execution-buffers&quot; aria-label=&quot;Anchor link for: the-blurry-line-between-planning-and-execution-buffers&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;While writing the articles for this blog I often face the doubts whatever I have everything correct. Because with a complex system like PostgreSQL one is always learning. Recently learned more about planning buffers based on my somewhat technically imprecise assumption.&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL actually does not resolve all metadata during the planning phase. The planner does the minimum work needed to choose the best plan, but defers some catalog lookups to execution time. When a Sort node first runs, it looks up the comparison function from &lt;code&gt;pg_amproc&lt;&#x2F;code&gt; via &lt;code&gt;get_opfamily_proc()&lt;&#x2F;code&gt;. That lookup hits shared buffers and gets counted as &lt;em&gt;execution&lt;&#x2F;em&gt; buffers. On the second run in the same session, the syscache already has that information - no buffer access, fewer reported buffers.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;putting-it-together&quot;&gt;Putting it together&lt;a class=&quot;zola-anchor&quot; href=&quot;#putting-it-together&quot; aria-label=&quot;Anchor link for: putting-it-together&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Here&#x27;s a sample output of a query with problems across every buffer category:&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; Buffers: shared hit=50 read=15000 written=847&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          temp read=2500 written=2500&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Planning:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Buffers: shared hit=12 read=156&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Planning Time: 45.678 ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Execution Time: 12345.678 ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Reading this top to bottom: the hit ratio is abysmal (50 hits vs 15,000 reads), so the working set isn&#x27;t cached. The &lt;code&gt;written=847&lt;&#x2F;code&gt; means the query forced synchronous evictions - the background writer can&#x27;t keep up. The temp spill points to an operation exceeding &lt;code&gt;work_mem&lt;&#x2F;code&gt;. Even planning needed 156 reads, suggesting system catalogs got evicted from cache.&lt;&#x2F;p&gt;
&lt;p&gt;Each number points to a specific tuning lever: &lt;code&gt;shared_buffers&lt;&#x2F;code&gt;, &lt;code&gt;bgwriter_lru_maxpages&lt;&#x2F;code&gt;, &lt;code&gt;work_mem&lt;&#x2F;code&gt;, or query optimization to touch less data.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;looking-beyond-single-queries&quot;&gt;Looking beyond single queries&lt;a class=&quot;zola-anchor&quot; href=&quot;#looking-beyond-single-queries&quot; aria-label=&quot;Anchor link for: looking-beyond-single-queries&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Single query analysis is useful, but patterns across your workload matter more. &lt;code&gt;pg_stat_statements&lt;&#x2F;code&gt; exposes the same buffer counters aggregated over 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;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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    substring&lt;&#x2F;span&gt;&lt;span&gt;(query, &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;60&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; query,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    calls,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    shared_blks_hit,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    shared_blks_read,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    round&lt;&#x2F;span&gt;&lt;span&gt;(&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;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span&gt; shared_blks_hit &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;      nullif&lt;&#x2F;span&gt;&lt;span&gt;(shared_blks_hit &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;+&lt;&#x2F;span&gt;&lt;span&gt; shared_blks_read, &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;2&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; hit_pct,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    temp_blks_written&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_stat_statements&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; calls &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; 100&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ORDER BY&lt;&#x2F;span&gt;&lt;span&gt; shared_blks_read &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DESC&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LIMIT&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;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This shows which queries are causing the most disk reads across your system - often more actionable than analyzing one query at a time.&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;Buffer statistics transform EXPLAIN from &quot;here&#x27;s the plan&quot; to &quot;here&#x27;s exactly where the time went.&quot; Every number points to a specific cause and a specific fix. Once you start reading them, you stop guessing and start tuning.&lt;&#x2F;p&gt;
&lt;p&gt;If you need to get bigger picture on buffer management, check out &lt;a href=&quot;&#x2F;posts&#x2F;introduction-to-buffers&#x2F;&quot;&gt;Introduction to Buffers in PostgreSQL&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Introduction to Buffers in PostgreSQL</title>
        <published>2026-01-24T16:57:00+00:00</published>
        <updated>2026-01-24T16:57:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/introduction-to-buffers/"/>
        <id>https://boringsql.com/posts/introduction-to-buffers/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/introduction-to-buffers/">&lt;p&gt;The work around &lt;a href=&quot;&#x2F;products&#x2F;regresql&#x2F;&quot;&gt;RegreSQL&lt;&#x2F;a&gt; led me to focus a lot on &lt;strong&gt;buffers&lt;&#x2F;strong&gt;. If you are a casual PostgreSQL user, you have probably heard about adjusting &lt;code&gt;shared_buffers&lt;&#x2F;code&gt; and followed the good old advice to set it to 1&#x2F;4 of available RAM. But after we went a little bit too enthusiastic about them on a recent &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;postgres.fm&#x2F;episodes&#x2F;regresql&quot;&gt;Postgres FM episode&lt;&#x2F;a&gt; I&#x27;ve been asked what that&#x27;s all about.&lt;&#x2F;p&gt;
&lt;p&gt;Buffers are one of those topics that easily gets forgotten. And while they are a foundation block of PostgreSQL&#x27;s performance architecture, most of us treat them as a black box. This article is going to attempt to change that.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-8kb-page&quot;&gt;The 8KB page&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-8kb-page&quot; aria-label=&quot;Anchor link for: the-8kb-page&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;There&#x27;s one concept we need to cover before diving into the buffers. And that&#x27;s the concept of the 8KB page. Everything in PostgreSQL is stored in blocks that are 8KB wide.&lt;&#x2F;p&gt;
&lt;p&gt;When PostgreSQL reads the data, it does not read individual rows. It reads the entire page. When it writes something, same thing - same page. You want to retrieve one small row, you will always retrieve much more data to go along with it. And if you followed carefully, same applies to writes.&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;-- you can check the block size (which should be almost always 8192 bytes)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;show block_size;&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&gt; block_size&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 style=&quot;color: #79B8FF;&quot;&gt; 8192&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;Every table and index is a collection of these pages. A row might span multiple pages if it&#x27;s large enough, but the page remains the atomic unit of I&#x2F;O.&lt;&#x2F;p&gt;
&lt;div class=&quot;callout&quot;&gt;
&lt;strong&gt;Want to look inside?&lt;&#x2F;strong&gt;&lt;br&gt;
If you want to see exactly how PostgreSQL packs your data, headers, and line pointers into these 8,192 bytes, check out the byte-level tour in &lt;a href=&quot;&#x2F;posts&#x2F;inside-the-8kb-page&#x2F;&quot;&gt;Inside PostgreSQL&#x27;s 8KB Page&lt;&#x2F;a&gt;.
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;postgresql-vs-os&quot;&gt;PostgreSQL vs OS&lt;a class=&quot;zola-anchor&quot; href=&quot;#postgresql-vs-os&quot; aria-label=&quot;Anchor link for: postgresql-vs-os&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The interesting part is understanding why PostgreSQL needs to maintain its own infrastructure for its own buffer cache, when the operating system can already cache disk pages.&lt;&#x2F;p&gt;
&lt;p&gt;The answer is quite simple. PostgreSQL understands the data it reads. Whilst the operating system only sees files and bytes. PostgreSQL sees tables, indexes, query plans and has semantic knowledge to cache things faster.&lt;&#x2F;p&gt;
&lt;p&gt;Consider this example: a query needs to perform a sequential scan of a large table. The OS might happily cache all those pages, but PostgreSQL knows this is a one-time operation and uses a special strategy (ring buffers) to avoid eviction of the main cache.&lt;&#x2F;p&gt;
&lt;p&gt;The second important aspect is ACID - or better, the guarantee that write-ahead log (WAL) reaches stable storage before the data page is modified. The OS does not differentiate and can&#x27;t effectively guarantee this durability requirement (only at the cost of performance impact).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shared-buffers&quot;&gt;shared_buffers&lt;a class=&quot;zola-anchor&quot; href=&quot;#shared-buffers&quot; aria-label=&quot;Anchor link for: shared-buffers&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Now we can move to what we all know is the main PostgreSQL &quot;cache&quot;. The &lt;code&gt;shared_buffers&lt;&#x2F;code&gt; parameter controls the size of the shared memory, accessible to all backend processes. If any backend needs to retrieve a page, it first checks the shared buffers. If the page is present - it&#x27;s a hit. No disk I&#x2F;O is needed. Miss? Read it from disk (or OS cache) and store it in shared buffers for next time.&lt;&#x2F;p&gt;
&lt;div class=&quot;sidenote&quot;&gt;WAL has its own buffer area (&lt;strong&gt;wal_buffers&lt;&#x2F;strong&gt;). This separation exists because WAL writes are sequential and must be persisted before the corresponding data change can be considered committed. The default is 3% of shared_buffers, capped at 16MB (one WAL segment).&lt;&#x2F;div&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;show shared_buffers;&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&gt; shared_buffers&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; 128MB&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;The default value 128MB is very conservative and is part of making sure the PostgreSQL default installation will work pretty much on any system (including those with limited RAM). But in your regular environment this value should typically be much higher.&lt;&#x2F;p&gt;
&lt;p&gt;The value of 128MB there refers to the actual buffered content. If you consider a single page being 8KB, you can imagine this as 16,384 individual slots for storing the data.&lt;&#x2F;p&gt;
&lt;div class=&quot;visualizer-banner&quot;&gt;
  &lt;div class=&quot;visualizer-banner__preview&quot;&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer dirty&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer pinned&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer dirty&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer empty&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer dirty&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer pinned&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer dirty&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer dirty&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer empty&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer pinned&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer dirty&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer dirty&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer pinned&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer empty&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer dirty&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer dirty&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer pinned&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer empty&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer dirty&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer pinned&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer dirty&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer dirty&quot;&gt;&lt;&#x2F;div&gt;
    &lt;div class=&quot;mini-buffer clean&quot;&gt;&lt;&#x2F;div&gt;
  &lt;&#x2F;div&gt;
  &lt;div class=&quot;visualizer-banner__content&quot;&gt;
    &lt;strong&gt;Interactive Visualizer&lt;&#x2F;strong&gt;
    &lt;p&gt;Explore how PostgreSQL&#x27;s buffer pool actually works. Watch the clock sweep algorithm evict cold pages, see buffers transition between clean, dirty, and pinned states, and understand how the hash table enables O(1) lookups.&lt;&#x2F;p&gt;
    &lt;a href=&quot;&#x2F;visualizers&#x2F;shared-buffers&#x2F;&quot; class=&quot;visualizer-banner__button&quot;&gt;Open Visualizer&lt;&#x2F;a&gt;
  &lt;&#x2F;div&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;However, there&#x27;s more to the buffer pool than just the pages themselves. PostgreSQL needs to track metadata and provide fast lookups, so the shared memory area is organized into three components:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;buffer blocks&lt;&#x2F;strong&gt; - the actual 8KB pages where the data lives&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;buffer descriptors&lt;&#x2F;strong&gt; - a parallel array of ~64-byte structures, one per slot.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;hash table&lt;&#x2F;strong&gt; used for mapping page identifiers to individual buffer slots.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Each descriptor then tracks which page is cached in the slot (tag), flags about the state (dirty, valid and I&#x2F;O in-progress), and pin&#x2F;usage counters.&lt;&#x2F;p&gt;
&lt;div class=&quot;sidenote&quot;&gt;O(1) = constant time, regardless of buffer pool size.&lt;&#x2F;div&gt;The hash table enables fast lookups. When a backend needs a specific page, it hashes the page identifier and jumps directly to the right bucket—no
  need to scan all 16,384 slots. This keeps buffer lookups at O(1) regardless of pool size.
&lt;p&gt;When a backend needs page N of table &lt;code&gt;orders&lt;&#x2F;code&gt;, it hashes the identifier, looks up the hash table - which drives the hit&#x2F;miss logic.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pin-and-usage-counts&quot;&gt;Pin and usage counts&lt;a class=&quot;zola-anchor&quot; href=&quot;#pin-and-usage-counts&quot; aria-label=&quot;Anchor link for: pin-and-usage-counts&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;What happens to each buffer slot is ruled by the two counters mentioned above: pin and usage count.&lt;&#x2F;p&gt;
&lt;p&gt;Pin count tracks active references. When a backend (for example a running query) is actively reading or modifying a page, it pins the buffer so it can&#x27;t be evicted. When the backend finishes, it unpins the buffer.&lt;&#x2F;p&gt;
&lt;p&gt;Usage count tracks how recently and frequently a buffer was accessed. Each access increments the count (capped at 5). During eviction, the clock
sweep decrements this value—buffers with higher counts survive longer, while those at 0 get evicted.&lt;&#x2F;p&gt;
&lt;p&gt;The usage counter is important to avoid behaviour when a single sequential scan would flush the entire buffer pool. Imagine reading a full 1GB table. Without this protection, it would evict everything in shared buffers, ignoring frequently accessed data. We will also touch on this specific behaviour later, in the ring buffers.&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL provides you functionality to examine what&#x27;s happening in the shared buffer cache in real time using extension &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;pgbuffercache.html&quot;&gt;pg_buffercache&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-clock-sweep-algorithm&quot;&gt;The clock sweep algorithm&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-clock-sweep-algorithm&quot; aria-label=&quot;Anchor link for: the-clock-sweep-algorithm&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Now that we covered tracking of the usage, what happens when PostgreSQL needs to load a page and all slots are taken? It needs to try to make some space.&lt;&#x2F;p&gt;
&lt;div class=&quot;sidenote&quot;&gt;A simple LRU check would represent much higher maintenance; updating the linked list on each buffer load would dramatically increase the complexity.&lt;&#x2F;div&gt;That&#x27;s where the clock sweep algorithm comes in. Why clock? Imagine the buffer pool as a circular clock, and the algorithm always moves forward, sweeping along the way. As it passes each slot it
&lt;ol&gt;
&lt;li&gt;Is the buffer pinned? Skips it as it&#x27;s being used.&lt;&#x2F;li&gt;
&lt;li&gt;Is the usage counter &amp;gt; 0, reduce it by one and continue to next slot (i.e. it survives till next round).&lt;&#x2F;li&gt;
&lt;li&gt;If the usage counter hits 0 and it&#x27;s not in use, it&#x27;s going to be evicted.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;The clock sweep is a simple way to ensure cold pages get evicted quickly, while hot pages tend to stick and survive multiple sweeps.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dirty-buffers-and-the-background-writer&quot;&gt;Dirty buffers and the background writer&lt;a class=&quot;zola-anchor&quot; href=&quot;#dirty-buffers-and-the-background-writer&quot; aria-label=&quot;Anchor link for: dirty-buffers-and-the-background-writer&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;div class=&quot;sidenote&quot;&gt;Don&#x27;t forget that a change is always first written to WAL.&lt;&#x2F;div&gt;Up until now we have discussed that a buffer loaded from disk is the same one in the shared buffers cache. When a backend modifies the page, the buffer becomes &quot;dirty&quot; but it&#x27;s not immediately written to storage. Dirty buffers represent I&#x2F;O work that has not yet happened. As it would be inefficient to write it immediately. The same page can be modified multiple times in short bursts, making the previous I&#x2F;O operations unnecessary.
&lt;p&gt;Instead dirty pages accumulate until one of the following events happen.&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL writes all dirty buffers to disk during &lt;strong&gt;checkpoint&lt;&#x2F;strong&gt;. This is a periodic process (which you can force using the &lt;code&gt;CHECKPOINT&lt;&#x2F;code&gt; command). This is a point where data on disk becomes consistent. After its successful completion, crash recovery only needs to replay write-ahead logs from that moment forward.&lt;&#x2F;p&gt;
&lt;p&gt;The second mechanism is &lt;strong&gt;the background writer&lt;&#x2F;strong&gt;. It continuously scans for dirty buffers and writes them before anyone needs to evict them. This way, when a backend runs clock sweep, it finds clean buffers ready to evict, without needing to wait for I&#x2F;O. It also spreads writes over time instead of bursty spikes during eviction pressure.&lt;&#x2F;p&gt;
&lt;p&gt;And as suggested the last chance to write the dirty pages to the disk is when the clock sweep finds a dirty buffer. It must write the data to disk so it can be reused for the new page. This is the worst case as it resorts to synchronous I&#x2F;O operation.&lt;&#x2F;p&gt;
&lt;p&gt;The ultimate goal is to balance the clean buffers available, and therefore preventing backends from being blocked by synchronous write during the eviction. If you followed carefully you can see how bad flow or settings can cause a problem. Eviction happens when new buffers are loaded (I&#x2F;O operation), and if the dirty buffer is evicted you force another I&#x2F;O operation.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ring-buffers&quot;&gt;Ring buffers&lt;a class=&quot;zola-anchor&quot; href=&quot;#ring-buffers&quot; aria-label=&quot;Anchor link for: ring-buffers&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;As mentioned above there are also special types of buffers. We already touched the scenario when a query performs the scans of a large table.&lt;&#x2F;p&gt;
&lt;p&gt;In a naive implementation, the sequential scan would load all data into shared buffers, evicting everything else along the way. Your warmed cache would vanish and subsequent queries would suffer for minutes to come.&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL&#x27;s solution for this case is &lt;strong&gt;ring buffers&lt;&#x2F;strong&gt;. Small, private buffer pools for bulk operations. Instead of using the shared buffer pool, certain operations get their own limited ring.&lt;&#x2F;p&gt;
&lt;p&gt;The individual cases are:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Sequential scans on large tables&lt;&#x2F;strong&gt; over 1&#x2F;4 of &lt;code&gt;shared_buffers&lt;&#x2F;code&gt; use a dedicated 256 KB ring buffer. Pages cycle through this tiny ring and never touch the main cache.&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;-- perform a sequential scan over a large table&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;EXPLAIN (ANALYZE, BUFFERS) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; count&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: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; ring_buffer_test;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Which would show you &lt;code&gt;Buffers: shared read=127285&lt;&#x2F;code&gt; instead of &lt;strong&gt;hit&lt;&#x2F;strong&gt;. I will follow with the separate article on how to read buffers in explain plans.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Bulk writes&lt;&#x2F;strong&gt; (COPY, CREATE TABLE AS) use a 16MB capped ring buffer, large enough for efficient batching, yet small enough not to pollute the shared buffer pool.&lt;&#x2F;p&gt;
&lt;p&gt;As &lt;strong&gt;VACUUM&lt;&#x2F;strong&gt; touches every page and shouldn&#x27;t evict the hot data, it uses its own dedicated ring buffer. Historically it was set to 256KB, but since PostgreSQL 17 it can be configured using &lt;code&gt;vacuum_buffer_usage_limit&lt;&#x2F;code&gt; (while previous two are given).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;local-buffers&quot;&gt;Local buffers&lt;a class=&quot;zola-anchor&quot; href=&quot;#local-buffers&quot; aria-label=&quot;Anchor link for: local-buffers&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The second kind of exception to shared buffer pool, is the session based &lt;strong&gt;temporary tables&lt;&#x2F;strong&gt;. In this case as the concurrency is out of question, each backend has its own local buffer pool, controlled by &lt;code&gt;temp_buffers&lt;&#x2F;code&gt; (8MB default).&lt;&#x2F;p&gt;
&lt;p&gt;Local buffers are faster than shared buffers because they have simpler locking. There is no need for the heavy cross-process coordination required in the main cache.&lt;&#x2F;p&gt;
&lt;p&gt;While this might seem like a minor implementation detail, it can translate into a powerful optimisation strategy. Many developers tend to default to complex &lt;a href=&quot;&#x2F;posts&#x2F;good-cte-bad-cte&#x2F;&quot;&gt;CTE logic&lt;&#x2F;a&gt; for intermediate data, but using temporary tables offers a distinct advantage in the form of &lt;strong&gt;lower I&#x2F;O&lt;&#x2F;strong&gt; as the changes to temporary tables are not WAL logged and by their nature reduce pollution of shared buffer pools.&lt;&#x2F;p&gt;
&lt;p&gt;If your workload involves large temporary tables, increasing temp_buffers can help keep those operations purely in RAM. However, remember that this memory is per-connection, so it multiplies across all backends.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-os-page-cache&quot;&gt;The OS Page Cache&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-os-page-cache&quot; aria-label=&quot;Anchor link for: the-os-page-cache&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;PostgreSQL doesn&#x27;t bypass the operating system. Every read and write goes through the kernel, which maintains its own page cache. This creates &lt;strong&gt;double buffering&lt;&#x2F;strong&gt; - the same 8KB page can exist in both PostgreSQL&#x27;s shared buffers and the OS cache simultaneously.&lt;&#x2F;p&gt;
&lt;div class=&quot;sidenote&quot;&gt;The OS cache acts as a &quot;Level 2 cache&quot; - when PostgreSQL evicts a page, it often still lives in OS memory.&lt;&#x2F;div&gt;
&lt;p&gt;This sounds wasteful, but it&#x27;s actually a feature. When PostgreSQL evicts a clean page to make room, that page drops into the OS cache rather than disappearing. A moment later, if you need it back, the OS serves it from RAM - no disk I&#x2F;O required.&lt;&#x2F;p&gt;
&lt;p&gt;The OS also provides &lt;strong&gt;read-ahead&lt;&#x2F;strong&gt; - detecting sequential access patterns and pre-loading pages before PostgreSQL requests them.&lt;&#x2F;p&gt;
&lt;p&gt;This relationship explains the classic advice: set &lt;code&gt;shared_buffers&lt;&#x2F;code&gt; to 25% of RAM. You&#x27;re deliberately leaving space for the OS to act as a safety net.&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;-- on dedicated servers with large RAM, 40% can work well&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- but always leave room for OS cache and other processes&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 SYSTEM SET&lt;&#x2F;span&gt;&lt;span&gt; shared_buffers &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;8GB&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;div class=&quot;sidenote&quot;&gt;This parameter allocates no memory - it&#x27;s purely a hint for cost estimation.&lt;&#x2F;div&gt;
&lt;p&gt;PostgreSQL needs to know about this combined cache for query planning. That&#x27;s where &lt;code&gt;effective_cache_size&lt;&#x2F;code&gt; comes in - it tells the planner how much total cache (shared buffers + OS) to assume when estimating costs.&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;-- estimate of total cache available (shared + OS)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SHOW effective_cache_size;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A higher value encourages the planner to favor index scans, assuming data is likely cached somewhere even if not in shared buffers.&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;Buffers are one of the keystones of PostgreSQL internals. They control whether the query will hit fast RAM or slow disk; and at the same time they play a crucial role in the fragile balance of dirty pages, WAL and durability.&lt;&#x2F;p&gt;
&lt;p&gt;The shared buffer pool isn&#x27;t just a cache - it&#x27;s a sophisticated memory manager with clock sweep eviction, usage count decay, ring buffer isolation, and background maintenance. Understanding these mechanisms helps you tune effectively and diagnose problems when they start.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The hidden cost of PostgreSQL arrays</title>
        <published>2026-01-12T20:50:00+00:00</published>
        <updated>2026-01-12T20:50:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/good-bad-arrays/"/>
        <id>https://boringsql.com/posts/good-bad-arrays/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/good-bad-arrays/">&lt;p&gt;Starting with arrays in PostgreSQL is as simple as declaring a column as &lt;code&gt;integer[]&lt;&#x2F;code&gt;, inserting some values, and you are done.&lt;&#x2F;p&gt;
&lt;p&gt;Or building the array on the fly.&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 style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;{1,2,3}&amp;#39;&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 style=&quot;color: #F97583;&quot;&gt;SELECT array&lt;&#x2F;span&gt;&lt;span&gt;[1,2,3];&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;  int4&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; {1,2,3}&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;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  array&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; {1,2,3}&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;The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;arrays.html&quot;&gt;official documentation&lt;&#x2F;a&gt; provides a good introduction. But beneath this straightforward interface lies a set of more complex properties than most of us realise. Arrays in PostgreSQL are not just &quot;lists&quot; in a field. They have their own memory management strategy, their own index logic, and a lot of edge-case scenarios.&lt;&#x2F;p&gt;
&lt;p&gt;As it goes with &lt;strong&gt;boringSQL&lt;&#x2F;strong&gt; deep-dives, this article will explore the corners of array functionality that might break your production.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-document-model-temptation&quot;&gt;The document model temptation&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-document-model-temptation&quot; aria-label=&quot;Anchor link for: the-document-model-temptation&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Wait? Are we going to talk about JSONB arrays? Not at all. The whole concept of arrays in RDBMSs is actually &lt;strong&gt;document storage in disguise&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;span class=&quot;sidenote&quot;&gt;In database design, locality ensures faster retrieval times by keeping related data close on physical storage.&lt;&#x2F;span&gt;Whether you use a distinct &lt;code&gt;integer[]&lt;&#x2F;code&gt; type or a JSON list &lt;code&gt;[1, 2, 3]&lt;&#x2F;code&gt;, you are making the exact same architectural decision: you are &lt;strong&gt;prioritising locality over normalisation&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;When you store &lt;code&gt;tag_ids&lt;&#x2F;code&gt; in an array, you are embedding related data directly into a row - just like a NoSQL database might embed subdocuments. This is not inherently wrong. Document databases exist for good reasons: they eliminate joins, simplify reads, and map naturally to application objects.&lt;&#x2F;p&gt;
&lt;p&gt;But PostgreSQL is a relational database. It was designed around the relational model, where:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;foreign keys&lt;&#x2F;strong&gt; enforce &lt;a href=&quot;&#x2F;posts&#x2F;text-identifier-in-db-design&#x2F;&quot;&gt;referential integrity&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;joins&lt;&#x2F;strong&gt; connect normalised tables&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;updates&lt;&#x2F;strong&gt; modify individual rows, not entire lists&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Arrays give you document-model convenience, but you lose relational promises. There are no foreign keys and no &lt;code&gt;ON DELETE referential_action&lt;&#x2F;code&gt; (like CASCADE) for array elements. If you delete a &lt;code&gt;tags&lt;&#x2F;code&gt; entry, the orphaned ID will remain in your array forever.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;The rule of thumb&lt;&#x2F;strong&gt; is that if you find yourself in need of referential integrity - you most likely want a link table, not an array. Arrays are for data that shares the same lifecycle as the parent row. Not for relationships spanning across different tables.&lt;&#x2F;p&gt;
&lt;p&gt;A practical example is the author of a blog post (one author might write multiple other posts), whereas a whitelist of IP addresses for a service account is only applicable to the given entity.&lt;&#x2F;p&gt;
&lt;p&gt;With JSONB being so flexible, you might wonder why we still bother with quirkier native types. The answer lies in the &#x27;boring&#x27; part of the database: predictability and efficiency. An &lt;code&gt;integer[]&lt;&#x2F;code&gt; column guarantees that every element is an integer — similar to how &lt;a href=&quot;&#x2F;posts&#x2F;postgresql-enums&#x2F;&quot;&gt;enums&lt;&#x2F;a&gt; enforce a fixed set of allowed values.&lt;&#x2F;p&gt;
&lt;p&gt;Arrays are also more storage-efficient for primitives because they don&#x27;t carry the metadata overhead of JSON objects.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-syntax-gotchas&quot;&gt;The syntax gotchas&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-syntax-gotchas&quot; aria-label=&quot;Anchor link for: the-syntax-gotchas&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;This post assumes basic knowledge of arrays. We won&#x27;t cover the basics.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;arrays-don-t-have-to-start-at-1&quot;&gt;Arrays don&#x27;t have to start at 1&lt;a class=&quot;zola-anchor&quot; href=&quot;#arrays-don-t-have-to-start-at-1&quot; aria-label=&quot;Anchor link for: arrays-don-t-have-to-start-at-1&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;By default, SQL arrays start at 1. And seemingly, there is nothing wrong with iterating through them in a fashion similar to:&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;FOR&lt;&#x2F;span&gt;&lt;span&gt; i &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span&gt; .. array_length(fruits, &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;LOOP&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    RAISE NOTICE &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Index % contains: %&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, i, fruits[i];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;END LOOP&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 is until you find an array with the arbitrary bounds. Which PostgreSQL allows.&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 style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;[-5:-3]={10,20,30}&amp;#39;&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;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;To make sure you iterate correctly through any given array, always use &lt;code&gt;array_lower()&lt;&#x2F;code&gt; and &lt;code&gt;array_upper()&lt;&#x2F;code&gt; in PL&#x2F;pgSQL&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; array_lower(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[-5:-3]={10,20,30}&amp;#39;&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;span style=&quot;color: #79B8FF;&quot;&gt;1&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; array_lower&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;          -5&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;or &lt;code&gt;generate_subscripts()&lt;&#x2F;code&gt; in SQL.&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; generate_subscripts(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[-5:-3]={10,20,30}&amp;#39;&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;span style=&quot;color: #79B8FF;&quot;&gt;1&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; generate_subscripts&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;                  -5&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                  -4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                  -3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(3 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;missing-dimensions&quot;&gt;Missing dimensions&lt;a class=&quot;zola-anchor&quot; href=&quot;#missing-dimensions&quot; aria-label=&quot;Anchor link for: missing-dimensions&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;When creating a table, you might expect strict typing. That&#x27;s true for everything — except the array dimensions. You might think &lt;code&gt;integer[][]&lt;&#x2F;code&gt; enforces a 2D matrix. Except it does not. The &lt;code&gt;[]&lt;&#x2F;code&gt; syntax is effectively syntactic sugar. PostgreSQL does not enforce the number of dimensions of sub-arrays at the schema level at all by default.&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; dimension_test&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;    matrix &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&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; dimension_test &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;{{1,2}, {3,4}}&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- this is not going to fail&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; dimension_test &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;{1,2,3}&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- 3D matrix works too&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; dimension_test &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;{{{1,2},{3,4}}, {{5,6},{7,8}}}&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;If you want to enforce a specific array dimension, you cannot rely on the type definition. Instead, you must use a &lt;code&gt;CHECK&lt;&#x2F;code&gt; constraint.&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; strict_matrix&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: #6A737D;&quot;&gt;    -- dims: ensure it&amp;#39;s 2-Dimensional&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    -- array_length: make sure it&amp;#39;s exactly 3x3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    board &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;span style=&quot;color: #F97583;&quot;&gt; CHECK&lt;&#x2F;span&gt;&lt;span&gt; (array_ndims(board) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AND&lt;&#x2F;span&gt;&lt;span&gt; array_length(board, &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;=&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&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;INSERT INTO strict_matrix VALUES (&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ARRAY[&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        [0, 1, 0],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        [1, 1, 0],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        [0, 0, 1]&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;The only exception is that PostgreSQL enforces uniformity of arrays on every nesting level. This means it rejects sub-arrays with different sizes.&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;INSERT INTO&lt;&#x2F;span&gt;&lt;span&gt; dimension_test &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;{{1,2}, {3}}&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;ERROR:  malformed &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;array&lt;&#x2F;span&gt;&lt;span&gt; literal: &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;{{1,2}, {3}}&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LINE&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;INSERT INTO&lt;&#x2F;span&gt;&lt;span&gt; dimension_test &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;{{1,2}, {3}}&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;                                           ^&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;DETAIL:  Multidimensional arrays must have sub&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span&gt;arrays &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;with&lt;&#x2F;span&gt;&lt;span&gt; matching dimensions.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;slicing-arrays&quot;&gt;Slicing arrays&lt;a class=&quot;zola-anchor&quot; href=&quot;#slicing-arrays&quot; aria-label=&quot;Anchor link for: slicing-arrays&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;When accessing array values, it&#x27;s important to consider that the syntax &lt;code&gt;[1]&lt;&#x2F;code&gt; and &lt;code&gt;[1:1]&lt;&#x2F;code&gt; are different. While the first one is an accessor, the second one acts like a constructor.&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; matrix[1][1]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; from&lt;&#x2F;span&gt;&lt;span&gt; dimension_test ;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; matrix&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 style=&quot;color: #79B8FF;&quot;&gt;      1&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;When slicing an array, even if the slice is a single-element, it will be returned as a single-element array, not a scalar value.&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; matrix[1:1][1:1], matrix[1][1:1], matrix[1:1][1]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; from&lt;&#x2F;span&gt;&lt;span&gt; dimension_test ;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; matrix | matrix | matrix&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;1&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;&#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;h3 id=&quot;the-ugly&quot;&gt;The ugly&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-ugly&quot; aria-label=&quot;Anchor link for: the-ugly&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Accessing array values has a forgiving behaviour, making it more difficult to find underlying bugs:&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;-- out-of-bound access returns NULL&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;ARRAY&lt;&#x2F;span&gt;&lt;span&gt;[1,2,3])[10];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; array&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: #F97583;&quot;&gt;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;(&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;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;-- out of bounds slicing returns an empty array&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;ARRAY&lt;&#x2F;span&gt;&lt;span&gt;[1,2,3])[5:10];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; array&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;&#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;But the single most confusing aspect that might trip you up coming from other programming languages is the fact that PostgreSQL treats multi-dimensional arrays as a single matrix, not an array of arrays.&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;-- wrong dimensionality &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;ARRAY&lt;&#x2F;span&gt;&lt;span&gt;[[1,2],[3,4]])[1];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; array&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: #F97583;&quot;&gt;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;(&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;While in other languages you expect &lt;code&gt;{1,2}&lt;&#x2F;code&gt; as a result, PostgreSQL can&#x27;t give it to you. It tries to return the first cell and fails (because the index is not complete).&lt;&#x2F;p&gt;
&lt;p&gt;And to paraphrase Fletcher&#x27;s &quot;Double Bubble&quot; analogy (which also went very wrong — extra points for getting the reference), you can&#x27;t fix this by using slice notation.&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; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;{{1,2},{3,4}}&amp;#39;&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;[])[1:1];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  int4&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;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: #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;The only way to solve this puzzle is to &lt;code&gt;unnest&lt;&#x2F;code&gt; the slice and re-aggregate the results.&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; array_agg(val) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; unnest((&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;{{1,2},{3,4}}&amp;#39;&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;[])[1:1]) val;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; array_agg&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;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: #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;div class=&quot;callout&quot;&gt;
Just be aware that `array_agg` does not guarantee the order of aggregated elements unless you use an `ORDER BY` clause. While it usually works with simple `unnest` queries, relying on implicit ordering can be risky.
&lt;&#x2F;div&gt;
&lt;p&gt;The other alternative is to cast it to JSONB and back. Not pretty no matter how you look at it. If you need to work with complex multi-dimensional structures where each sub-array has independent meaning, just use JSONB. It will do exactly what you expect.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;indexing-arrays&quot;&gt;Indexing arrays&lt;a class=&quot;zola-anchor&quot; href=&quot;#indexing-arrays&quot; aria-label=&quot;Anchor link for: indexing-arrays&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;While you can use a B-tree index on an array column, it won&#x27;t help you unless you are looking for whole-array equality and sorting an array by dictionary rules — and even on regular columns, &lt;a href=&quot;&#x2F;posts&#x2F;why-postgresql-indexes-are-ignored&#x2F;&quot;&gt;PostgreSQL can ignore your indexes&lt;&#x2F;a&gt; when it decides a sequential scan is cheaper.&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;-- which is bigger? B is correct&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- A: {1, 1000, 1000}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- B: {2, 0}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Rendering B-tree indexes useless for any real-world index operations.&lt;&#x2F;p&gt;
&lt;p&gt;When working with arrays, you actually need &lt;strong&gt;GIN&lt;&#x2F;strong&gt; (Generalized Inverted Index). If a B-tree index is a phone book, GIN is an index at the back of the book. To query one or more elements, you need to find all possible locations and then intersect them to find the locations that match.&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 INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; posts_by_tags&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; posts &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;USING&lt;&#x2F;span&gt;&lt;span&gt; GIN (tags);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;GIN indexes are designed for set operations, making presence the key feature, while ignoring order. Here are operators that GIN provides for arrays:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Containment&lt;&#x2F;strong&gt; &lt;code&gt;@&amp;gt;&lt;&#x2F;code&gt; - matches rows that include ALL of the selected items.&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;-- match ALL tags&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;tags @&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;{urgent, bug}&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Overlap&lt;&#x2F;strong&gt; &lt;code&gt;&amp;amp;&amp;amp;&lt;&#x2F;code&gt; - does the row include ANY of the selected items.&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;-- match bug or feature&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;tags &amp;amp;&amp;amp; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;{bug, feature}&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;There&#x27;s also &lt;code&gt;&amp;lt;@&lt;&#x2F;code&gt; and &lt;code&gt;=&lt;&#x2F;code&gt; (equality), but they should be easy to understand.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;two-sides-of-any&quot;&gt;Two sides of &lt;code&gt;ANY&lt;&#x2F;code&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#two-sides-of-any&quot; aria-label=&quot;Anchor link for: two-sides-of-any&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;This is where it gets interesting. While you might often hear (myself included) that you shouldn&#x27;t use dynamic SQL for &lt;code&gt;IN&lt;&#x2F;code&gt; lists and should use &lt;code&gt;ANY&lt;&#x2F;code&gt; instead, there are some dangers you need to be aware of.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;code&gt;ANY&lt;&#x2F;code&gt; operator behaves very differently depending on which side of the comparison the array sits.&lt;&#x2F;p&gt;
&lt;p&gt;The advice to use &lt;code&gt;ANY&lt;&#x2F;code&gt; holds true when you are &lt;strong&gt;passing lists into the database&lt;&#x2F;strong&gt;. Instead of generating a query with 100 distinct parameters (&lt;code&gt;WHERE id IN ($1, $2, ... $100)&lt;&#x2F;code&gt;), which bloats your query cache and forces hard-parses, you should pass a single array parameter.&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;-- good: one parameter, one query plan&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; users &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;=&lt;&#x2F;span&gt;&lt;span&gt; ANY($&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;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The trap is assuming this syntax is equally efficient when querying array columns. It is not. If you use &lt;code&gt;ANY&lt;&#x2F;code&gt; to check if a value exists inside a table column, you are effectively asking the database to loop.&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;-- bad: GIN does not support ANY; turning this into a seq scan&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; tickets &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;feature&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; ANY(tags);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;When you write &lt;code&gt;WHERE &#x27;feature&#x27; = ANY(tags)&lt;&#x2F;code&gt;, you are not actually using an array operator. What you wrote is a scalar integer equality operator &lt;code&gt;=&lt;&#x2F;code&gt; applied inside a loop construct. Since the scalar operator &lt;code&gt;=&lt;&#x2F;code&gt; is not part of &lt;code&gt;array_ops&lt;&#x2F;code&gt;, the planner assumes the index cannot help and falls back to a sequential scan.&lt;&#x2F;p&gt;
&lt;p&gt;The correct way to rewrite the query is:&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;-- good again&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; tickets &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; tags @&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt; ARRAY&lt;&#x2F;span&gt;&lt;span&gt;[&amp;#39;feature&amp;#39;];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;fast-updates-and-the-trade-off&quot;&gt;Fast updates and the trade-off&lt;a class=&quot;zola-anchor&quot; href=&quot;#fast-updates-and-the-trade-off&quot; aria-label=&quot;Anchor link for: fast-updates-and-the-trade-off&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Because a GIN index is built to work with sets, it is expensive to maintain. With a B-tree index, one row equals one index entry. In a GIN index, one row equals N index entries, where N is the number of elements in your array.&lt;&#x2F;p&gt;
&lt;p&gt;This leads to write multiplication. To prevent this, PostgreSQL defaults to using a &quot;fast update&quot; mechanism. This is a strategy where new entries are added to a pending list (an unsorted temporary buffer) and only merged into the main index structure later (during VACUUM).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;span class=&quot;sidenote&quot;&gt;Given the unsorted nature of a GIN index, it is an exception to &lt;a href=&quot;&#x2F;posts&#x2F;vacuum-is-lie&#x2F;&quot;&gt;VACUUM is a Lie&lt;&#x2F;a&gt;, as VACUUM actually does perform structural maintenance here.&lt;&#x2F;span&gt;While this makes INSERT operations manageable, it can slow down SELECTs. Every time you query the index, PostgreSQL must scan the organised main index plus the entire messy pending list. If that list grows large, your query performance might degrade.&lt;&#x2F;p&gt;
&lt;p&gt;If you operate a read-heavy workflow with infrequent writes, you should disable this to guarantee consistent reading performance.&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 INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; posts_by_tags&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; posts &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;USING&lt;&#x2F;span&gt;&lt;span&gt; GIN (tags) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WITH&lt;&#x2F;span&gt;&lt;span&gt; (fastupdate &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;= off&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;storage-and-modification&quot;&gt;Storage and modification&lt;a class=&quot;zola-anchor&quot; href=&quot;#storage-and-modification&quot; aria-label=&quot;Anchor link for: storage-and-modification&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Now it&#x27;s time to return to &lt;a href=&quot;https:&#x2F;&#x2F;boringsql.com&#x2F;posts&#x2F;good-bad-arrays&#x2F;#the-document-model-temptation&quot;&gt;the document model&lt;&#x2F;a&gt;. In PostgreSQL, rows are immutable (MVCC); there is no such thing as an &quot;in-place update&quot;, and arrays are stored as atomic values. This results in a very uncomfortable truth — &lt;strong&gt;to modify a single element of an array, PostgreSQL must copy and rewrite the entire row&lt;&#x2F;strong&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;UPDATE&lt;&#x2F;span&gt;&lt;span&gt; user_activity&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; event_ids &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; event_ids &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;||&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 10001&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; user_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 50&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;Every single append rewrites the entire array, which in effect results in a rewrite of the entire row.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;toasted&quot;&gt;TOASTed&lt;a class=&quot;zola-anchor&quot; href=&quot;#toasted&quot; aria-label=&quot;Anchor link for: toasted&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;When any array grows large enough (&amp;gt; 2 KB — see below), PostgreSQL moves it automatically to a separate storage area using &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;storage-toast.html&quot;&gt;TOAST&lt;&#x2F;a&gt;. While this keeps the data row lean, it turns array updates into a severe performance bottleneck.&lt;&#x2F;p&gt;
&lt;p&gt;The difference this introduces is subtle but comes with a big impact. While a standard MVCC update simply copies the row version on the main heap, updating a TOASTed array forces PostgreSQL to fetch all external chunks, decompress the entire object into memory, apply the change, and then recompress and write the new full-size blob back to the TOAST table. This turns a simple modification into a CPU and I&#x2F;O-intensive operation that rewrites the entire dataset rather than just the delta.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;span class=&quot;sidenote&quot;&gt;The threshold is derived from &lt;code&gt;TOAST_TUPLES_PER_PAGE&lt;&#x2F;code&gt; (default: 4), ensuring 4 tuples fit on a page.&lt;&#x2F;br&gt;&lt;&#x2F;br&gt;Threshold: ~2 KB&lt;&#x2F;span&gt;Where does the 2 KB value come from? The TOAST threshold is calculated to ensure at least four tuples can fit on a single &lt;a href=&quot;&#x2F;posts&#x2F;inside-the-8kb-page&#x2F;&quot;&gt;8 KB heap page&lt;&#x2F;a&gt;. PostgreSQL uses this to balance efficiency against the overhead of TOAST indirection.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-compressions&quot;&gt;The compressions&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-compressions&quot; aria-label=&quot;Anchor link for: the-compressions&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Before version 14, PostgreSQL relied on &lt;code&gt;pglz&lt;&#x2F;code&gt; — an algorithm prioritising compression ratio over speed. This made the &quot;decompress-modify-compress&quot; cycle of TOAST painful.&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL 14 introduced LZ4 as an alternative:&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 TABLE&lt;&#x2F;span&gt;&lt;span&gt; articles &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ALTER&lt;&#x2F;span&gt;&lt;span&gt; COLUMN tags &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET COMPRESSION&lt;&#x2F;span&gt;&lt;span&gt; lz4;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;LZ4 is significantly faster for both compression and decompression, with only slightly lower compression ratios. If you are working with large arrays, switching to LZ4 is one of the easiest ways to reduce the CPU penalty of TOAST.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;when-a-large-array-might-make-sense&quot;&gt;When a large array might make sense&lt;a class=&quot;zola-anchor&quot; href=&quot;#when-a-large-array-might-make-sense&quot; aria-label=&quot;Anchor link for: when-a-large-array-might-make-sense&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;You might have gained the impression that arrays are bad. When evaluating the use of arrays, the real &lt;strong&gt;question isn&#x27;t how big the array is&lt;&#x2F;strong&gt; but rather &lt;strong&gt;how often do you modify it&lt;&#x2F;strong&gt;? An array of 10,000 elements that you write once and is read-only for the rest of its lifecycle is a completely valid use case. An array of 50 elements that you append to on every incoming request is the real villain here.&lt;&#x2F;p&gt;
&lt;p&gt;If you combine this with compression, you might get an interesting mix.&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;DROP TABLE IF EXISTS&lt;&#x2F;span&gt;&lt;span&gt; compression_test;&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 TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; compression_test&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;    compressed_floats float4[], &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    raw_floats float4[]&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;-- do not compress raw_floats&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; compression_test &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ALTER&lt;&#x2F;span&gt;&lt;span&gt; COLUMN raw_floats &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; STORAGE &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;EXTERNAL&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;-- insert semi-random data with low cardinality&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; compression_test (compressed_floats, raw_floats)&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; semi_random_arr, semi_random_arr&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; (&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 ARRAY&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;        SELECT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; floor&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; 50&lt;&#x2F;span&gt;&lt;span&gt;)::float4 &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;span&gt;    ) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;as&lt;&#x2F;span&gt;&lt;span&gt; semi_random_arr&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;as&lt;&#x2F;span&gt;&lt;span&gt; generator;&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;SELECT&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;Compressed (EXTENDED)&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; strategy,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pg_size_pretty(pg_column_size(compressed_floats)::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;bigint&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; size_on_disk&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; compression_test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;UNION ALL&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;Raw (EXTERNAL)&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;    pg_size_pretty(pg_column_size(raw_floats)::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;bigint&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&gt; compression_test;&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;       strategy        | size_on_disk&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; Compressed (EXTENDED) | 15 kB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Raw (EXTERNAL)        | 39 kB&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;h2 id=&quot;bulk-loading-with-arrays&quot;&gt;Bulk loading with arrays&lt;a class=&quot;zola-anchor&quot; href=&quot;#bulk-loading-with-arrays&quot; aria-label=&quot;Anchor link for: bulk-loading-with-arrays&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Up until now, it might seem that arrays don&#x27;t actually bring many benefits. While they can have rough edges around storage, they are incredibly useful for transport.&lt;&#x2F;p&gt;
&lt;p&gt;The fastest way to insert 5,000 rows isn&#x27;t a loop in your application, and it&#x27;s definitely not a massive &lt;code&gt;VALUES (...), (...)&lt;&#x2F;code&gt; string. It&#x27;s &lt;code&gt;unnest&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;INSERT INTO&lt;&#x2F;span&gt;&lt;span&gt; measurements (sensor_id, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;value&lt;&#x2F;span&gt;&lt;span&gt;, captured_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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; unnest(&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: #F97583;&quot;&gt;int&lt;&#x2F;span&gt;&lt;span&gt;[],        &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- array of sensor IDs&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;2&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;float&lt;&#x2F;span&gt;&lt;span&gt;[],      &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- array of values&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;3&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz&lt;&#x2F;span&gt;&lt;span&gt;[]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; -- array of timestamps&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;All that is needed is one network round trip, and one query to parse and plan. PostgreSQL handles the arrays row by row internally for you. This works both for UPSERTs and &lt;a href=&quot;&#x2F;posts&#x2F;beyond-upserts-with-merge&#x2F;&quot;&gt;MERGE&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-cases-for-special-arrays&quot;&gt;The cases for special arrays&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-cases-for-special-arrays&quot; aria-label=&quot;Anchor link for: the-cases-for-special-arrays&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Standard PostgreSQL arrays are polymorphic types (&lt;code&gt;anyarray&lt;&#x2F;code&gt;). This provides a powerful feature that allows a single function definition to operate on many different data types. They have to handle integers, strings, timestamps, and custom types equally well. But if you have specific data types, you can unlock significant performance gains by using specialised extensions.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;the-intarray-extension&quot;&gt;The &lt;code&gt;intarray&lt;&#x2F;code&gt; extension&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-intarray-extension&quot; aria-label=&quot;Anchor link for: the-intarray-extension&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;If you are dealing exclusively with 4-byte integers (&lt;code&gt;int4&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;integer&lt;&#x2F;code&gt;), the built-in array operations are leaving performance on the table. The &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;intarray.html&quot;&gt;intarray&lt;&#x2F;a&gt; extension provides specialised functions and index operators that are significantly faster than the generic implementation.&lt;&#x2F;p&gt;
&lt;p&gt;To use it, you must explicitly enable it:&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 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IF NOT EXISTS&lt;&#x2F;span&gt;&lt;span&gt; intarray;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The difference in developer ergonomics is immediate. To sort an array in standard SQL, you are forced to unnest, order, and re-aggregate. With intarray, you get native functions like sort() and uniq().&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;-- standard arrays&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; array_agg(val &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ORDER BY&lt;&#x2F;span&gt;&lt;span&gt; val)&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; unnest(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;{3, 1, 2}&amp;#39;&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;[]) val;&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;-- intarray&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; sort(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;{3, 1, 2}&amp;#39;&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;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Beyond raw management functions, it introduces a specialised query syntax that simplifies complex boolean logic. Instead of chaining multiple overlap (&lt;code&gt;&amp;amp;&amp;amp;&lt;&#x2F;code&gt;) and containment (&lt;code&gt;@&amp;gt;&lt;&#x2F;code&gt;) checks, you can express your requirements in a single &quot;query string&quot; using the &lt;code&gt;@@&lt;&#x2F;code&gt; operator.&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;-- standard arrays&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; staff&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; available_days @&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;{1}&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;          -- must include Mon&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  AND&lt;&#x2F;span&gt;&lt;span&gt; available_days &amp;amp;&amp;amp; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;{6, 7}&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;       -- must include Sat OR Sun&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  AND NOT&lt;&#x2F;span&gt;&lt;span&gt; (available_days @&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;{2}&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);   &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- must NOT include Tue&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;-- intarray&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; staff&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; available_days @@ &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;1 &amp;amp; (6 | 7) &amp;amp; !2&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;The only catch is the type restriction. &lt;code&gt;intarray&lt;&#x2F;code&gt; is strictly limited to signed 32-bit integers. If your values exceed 2 billion, you are back where you started.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ai-with-pgvector&quot;&gt;AI with &lt;code&gt;pgvector&lt;&#x2F;code&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#ai-with-pgvector&quot; aria-label=&quot;Anchor link for: ai-with-pgvector&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;You cannot talk about arrays in 2026 without mentioning &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pgvector&#x2F;pgvector&quot;&gt;pgvector&lt;&#x2F;a&gt;. While it markets itself as a &quot;vector store&quot;, internally it is simply an array of floats with a different mathematical focus.&lt;&#x2F;p&gt;
&lt;p&gt;Standard arrays are &lt;strong&gt;binary&lt;&#x2F;strong&gt;: they care about Exact Matches (Overlap &lt;code&gt;&amp;amp;&amp;amp;&lt;&#x2F;code&gt;, Containment &lt;code&gt;@&amp;gt;&lt;&#x2F;code&gt;). Vectors are all about &lt;strong&gt;fuzzy distance&lt;&#x2F;strong&gt; (Cosine &lt;code&gt;&amp;lt;=&amp;gt;&lt;&#x2F;code&gt;, Euclidean &lt;code&gt;&amp;lt;-&amp;gt;&lt;&#x2F;code&gt;).&lt;&#x2F;p&gt;
&lt;p&gt;If you are building search or recommendation features, pgvector allows you to treat your array column not as a list of &quot;facts&quot; (tag A, tag B), but as coordinates in semantic space.&lt;&#x2F;p&gt;
&lt;p&gt;Nevertheless, the architectural decision is exactly the same as using a standard array: you are trading strict structure for convenience. Since there is no way to &quot;join&quot; two rows based on how similar they are, you store the vector directly on the row. You accept a larger table size in exchange for the ability to ask, &quot;What is close to this?&quot;.&lt;&#x2F;p&gt;
&lt;p&gt;PS: Thanks to Matthias Feist for inspiring this article.&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>VACUUM Is a Lie (About Your Indexes)</title>
        <published>2025-12-11T23:34:00+00:00</published>
        <updated>2025-12-11T23:34:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/vacuum-is-lie/"/>
        <id>https://boringsql.com/posts/vacuum-is-lie/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/vacuum-is-lie/">&lt;p&gt;There is common misconception that troubles most developers using PostgreSQL:
tune VACUUM or run VACUUM, and your database will stay healthy. Dead tuples will
get cleaned up. Transaction IDs recycled. Space reclaimed. Your database will
live happily ever after.&lt;&#x2F;p&gt;
&lt;p&gt;But there are couple of dirty &quot;secrets&quot; people are not aware of. First of them
being &lt;strong&gt;VACUUM is lying to you about your indexes&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-anatomy-of-storage&quot;&gt;The anatomy of storage&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-anatomy-of-storage&quot; aria-label=&quot;Anchor link for: the-anatomy-of-storage&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;When you delete a row in PostgreSQL, it is just marked as a &#x27;dead tuple&#x27;.
Invisible for new transactions but still physically present. Only when all
transactions referencing the row are finished, VACUUM can come along and actually
remove them - reclamining the space in the heap (table) space.&lt;&#x2F;p&gt;
&lt;p&gt;To understand why this matters differently for tables versus indexes, you need
to picture how PostgreSQL actually stores your data.&lt;&#x2F;p&gt;
&lt;p&gt;Your table data lives in the heap - a collection of 8 KB pages where rows are
stored wherever they fit. There&#x27;s no inherent order. When you INSERT a row,
PostgreSQL finds a page with enough free space and slots the row in. Delete a
row, and there&#x27;s a gap. Insert another, and it might fill that gap - or not - they
might fit somewhere else entirely.&lt;&#x2F;p&gt;
&lt;p&gt;This is why &lt;code&gt;SELECT * FROM users&lt;&#x2F;code&gt; without an ORDER BY can return rows in order
initially, and after some updates in seemingly random order, and that order can
change over time. The heap is like Tetris. Rows drop into whatever space is
available, leaving gaps when deleted.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;images&#x2F;posts&#x2F;heap_page.png&quot; alt=&quot;Heap Page&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;When VACUUM runs, it removes those dead tuples and compacts the remaining
rows within each page. If an entire page becomes empty, PostgreSQL can reclaim
it entirely.&lt;&#x2F;p&gt;
&lt;p&gt;And while indexes are on surface the same collection of 8KB pages, they are
different. A B-tree index must maintain sorted order - that&#x27;s the
whole point of their existence and the reason why &lt;code&gt;WHERE id = 12345&lt;&#x2F;code&gt; is so
fast — though &lt;a href=&quot;&#x2F;posts&#x2F;why-postgresql-indexes-are-ignored&#x2F;&quot;&gt;even indexes can be ignored&lt;&#x2F;a&gt; under certain conditions. PostgreSQL can binary-search down the tree instead of scanning every
possible row. You can learn more about the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;use-the-index-luke.com&#x2F;sql&#x2F;anatomy&#x2F;the-tree&quot;&gt;fundamentals of B-Tree Indexes and
what makes them fast&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;But if the design of the indexes is what makes them fast, it&#x27;s also their
biggest responsibility. While PostgreSQL can fit rows into whatever space is
available, it can&#x27;t move the entries in index pages to fit as much as possible.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;&#x2F;images&#x2F;posts&#x2F;leaf_page.png&quot; alt=&quot;Leaf Page&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;VACUUM can remove dead index entries. But it doesn&#x27;t restructure the B-tree.
When VACUUM processes the heap, it can compact rows within a page and reclaim
empty pages. The heap has no ordering constraint - rows can be anywhere. But
B-tree pages? They&#x27;re locked into a structure. VACUUM can remove dead index
entries, yes.&lt;&#x2F;p&gt;
&lt;p&gt;Many developers assume VACUUM treats all pages same. No matter whether they are
heap or index pages. VACUUM is supposed to remove the dead entries, right?&lt;&#x2F;p&gt;
&lt;p&gt;Yes. But here&#x27;s what it doesn&#x27;t do - &lt;strong&gt;it doesn&#x27;t restructure the B-tree&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;What VACUUM actually does&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Removes dead tuple pointers from index pages&lt;&#x2F;li&gt;
&lt;li&gt;Marks completely empty pages as reusable&lt;&#x2F;li&gt;
&lt;li&gt;Updates the free space map&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;What VACUUM cannot do&lt;&#x2F;strong&gt;:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Merge sparse pages together (can do it for empty pages)&lt;&#x2F;li&gt;
&lt;li&gt;Reduce tree depth&lt;&#x2F;li&gt;
&lt;li&gt;Deallocate empty-but-still-linked pages&lt;&#x2F;li&gt;
&lt;li&gt;Change the physical structure of the B-tree&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Your heap is Tetris, gaps can get filled. Your B-tree is a sorted bookshelf.
VACUUM can pull books out, but can&#x27;t slide the remaining ones together. You&#x27;re
left walking past empty slots every time you scan.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-experiment&quot;&gt;The experiment&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-experiment&quot; aria-label=&quot;Anchor link for: the-experiment&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Let&#x27;s get hands-on and create a table, fill it, delete most of it and watch what happens.&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 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IF NOT EXISTS&lt;&#x2F;span&gt;&lt;span&gt; pgstattuple;&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 TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; demo&lt;&#x2F;span&gt;&lt;span&gt; (id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;integer PRIMARY KEY&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;data 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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- insert 100,000 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; demo (id, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;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 style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; g, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Row number &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span&gt; g &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; with some extra data&amp;#39;&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;100000&lt;&#x2F;span&gt;&lt;span&gt;) g;&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&gt;ANALYZE demo;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;At this point, our index is healthy. Let&#x27;s capture the baseline:&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    relname,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pg_size_pretty(pg_relation_size(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;oid&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; file_size,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pg_size_pretty((pgstattuple(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;oid&lt;&#x2F;span&gt;&lt;span&gt;)).tuple_len) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;as&lt;&#x2F;span&gt;&lt;span&gt; actual_data&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&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;IN&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;demo&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;demo_pkey&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  | file_size | actual_data&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;demo      | 7472 kB   | 6434 kB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;demo_pkey | 2208 kB   | 1563 kB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now remove some data, 80% to be precise - somewhere in the middle:&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;DELETE FROM&lt;&#x2F;span&gt;&lt;span&gt; demo &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;BETWEEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 10001&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AND&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 90000&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;The goal is to simulate a common real-world pattern: data retention policies,
bulk cleanup operations, or the aftermath of a &lt;a href=&quot;&#x2F;posts&#x2F;deletes-are-difficult&#x2F;&quot;&gt;data migration gone wrong&lt;&#x2F;a&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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;VACUUM demo;&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&gt;SELECT&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    relname,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pg_size_pretty(pg_relation_size(oid)) as file_size,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pg_size_pretty((pgstattuple(oid)).tuple_len) as actual_data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;FROM pg_class&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;WHERE relname IN (&amp;#39;demo&amp;#39;, &amp;#39;demo_pkey&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;relname  | file_size | actual_data&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;demo      | 7472 kB   | 1278 kB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;demo_pkey | 2208 kB   | 313 kB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The table shrunk significantly, while index remained unchanged You now have
20,000 rows indexed by a structure built to handle 100,000.&lt;&#x2F;p&gt;
&lt;p&gt;But notice something important: even though &lt;code&gt;actual_data&lt;&#x2F;code&gt; dropped to ~1.3 MB
(reflecting our 20,000 remaining rows), the &lt;code&gt;file_size&lt;&#x2F;code&gt; for the index is still
2208 kB. VACUUM cleaned out the dead tuple data, but it doesn&#x27;t return space to
the OS or compact index structures - it only marks pages as reusable within
PostgreSQL.&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;EDIT: the original table had a size without running VACCUM, which I must left
out during what&#x27;s sometime cumbersome process of authoring blog posts. Big
thanks come to Creston Jamison of &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.scalingpostgres.com&#x2F;&quot;&gt;Scaling
PostgreSQL&lt;&#x2F;a&gt; to notice it, and actually take
what must have been a lot of time to analyze my posts.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;This experiment is really an extreme case, but demonstrates the problem.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;understanding-page-states&quot;&gt;Understanding page states&lt;a class=&quot;zola-anchor&quot; href=&quot;#understanding-page-states&quot; aria-label=&quot;Anchor link for: understanding-page-states&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Leaf pages have several states:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Full page (&amp;gt;80% density)&lt;&#x2F;strong&gt;, when the page contains many index entries,
efficiently utilizing space. Each &lt;a href=&quot;&#x2F;posts&#x2F;inside-the-8kb-page&#x2F;&quot;&gt;8KB page&lt;&#x2F;a&gt; read returns substantial useful data.
This is optimal state.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Partial page (40-80% density)&lt;&#x2F;strong&gt; with some wasted space, but still reasonably
efficient. Common at tree edges or after light churn. Nothing to be worried about.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Sparse page (&amp;lt;40% density)&lt;&#x2F;strong&gt; is mostly empty. You&#x27;re reading an 8KB page to
find a handful of entries. The I&#x2F;O cost is the same as a full page, but you get
far less value.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Empty page (0% density)&lt;&#x2F;strong&gt; with zero live entries, but the page still exists in
the tree structure. Pure overhead. You might read this page during a range scan
and find absolutely nothing useful.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;a-note-on-fillfactor&quot;&gt;A note on fillfactor&lt;a class=&quot;zola-anchor&quot; href=&quot;#a-note-on-fillfactor&quot; aria-label=&quot;Anchor link for: a-note-on-fillfactor&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;You might be wondering how can fillfactor help with this? It&#x27;s the setting you
can apply both for heap and leaf pages, and controls how full PostgreSQL packs the
pages during the data storage. The &lt;strong&gt;default value for B-tree indexes is 90%&lt;&#x2F;strong&gt;. This
leaves 10% of free space on each leaf page for future insertions.&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 INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; demo_index&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; demo(id) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WITH&lt;&#x2F;span&gt;&lt;span&gt; (fillfactor &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 70&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;A lower fillfactor (like 70%) leaves more room, which can reduce page splits
when you&#x27;re inserting into the middle of an index - useful for tables random index
column inserts or those with heavily updated index columns.&lt;&#x2F;p&gt;
&lt;p&gt;But if you followed carefully the anatomy of storage section, it doesn&#x27;t help
with the bloat problem. Quite the opposite. If you set lower fillfactor and then
delete majority of your rows, you actually start with more pages, and bigger
chance to end up with more sparse pages than partial pages.&lt;&#x2F;p&gt;
&lt;p&gt;Leaf page fillfactor is about optimizing for updates and inserts. It&#x27;s not a
solution for deletion or index-column update bloat.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-the-planner-gets-fooled&quot;&gt;Why the planner gets fooled&lt;a class=&quot;zola-anchor&quot; href=&quot;#why-the-planner-gets-fooled&quot; aria-label=&quot;Anchor link for: why-the-planner-gets-fooled&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;PostgreSQL&#x27;s query planner &lt;a href=&quot;&#x2F;posts&#x2F;postgresql-statistics&#x2F;&quot;&gt;estimates costs based on physical
statistics&lt;&#x2F;a&gt;, including the number of pages in an index.&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 ANALYZE &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; demo &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;BETWEEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 10001&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AND&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 90000&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 demo_pkey on demo  (cost=0.29..29.29 rows=200 width=41) (actual time=0.111..0.112 rows=0 loops=1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Index Cond: ((id &amp;gt;= 10001) AND (id &amp;lt;= 90000))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Planning Time: 1.701 ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Execution Time: 0.240 ms&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;While the execution is almost instant, you need to look behind the scenes. The
planner estimated 200 rows and got zero. It traversed the B-tree structure
expecting data that doesn&#x27;t exist. On a single query with warm cache, this is
trivial. Under production load with thousands of queries and cold pages,
you&#x27;re paying I&#x2F;O cost for nothing. Again and again.&lt;&#x2F;p&gt;
&lt;p&gt;If you dig further you discover much bigger problem.&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, reltuples::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;bigint as&lt;&#x2F;span&gt;&lt;span&gt; row_estimate, relpages &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;as&lt;&#x2F;span&gt;&lt;span&gt; page_estimate&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&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;IN&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;demo&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;demo_pkey&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  | row_estimate | page_estimate&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;demo      |        20000 |           934&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;demo_pkey |        20000 |           276&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The &lt;code&gt;relpages&lt;&#x2F;code&gt; value comes from the physical file size divided by the &lt;a href=&quot;&#x2F;posts&#x2F;introduction-to-buffers&#x2F;&quot;&gt;8 KB page
size&lt;&#x2F;a&gt;. PostgreSQL updates it during VACUUM and ANALYZE, but it reflects the
actual file on disk - not how much useful data is inside. Our index file is still
2.2 MB (276 pages × 8 KB), even though most pages are empty.&lt;&#x2F;p&gt;
&lt;p&gt;The planner sees 276 pages for 20,000 rows and calculates a very low
rows-per-page ratio. This is when planner can come to conclusion - &lt;em&gt;this index
is very sparse - let&#x27;s do a sequential scan instead&lt;&#x2F;em&gt;. Oops.&lt;&#x2F;p&gt;
&lt;p&gt;&quot;But wait,&quot; you say, &quot;doesn&#x27;t ANALYZE fix statistics?&quot;&lt;&#x2F;p&gt;
&lt;p&gt;Yes and no. &lt;code&gt;ANALYZE&lt;&#x2F;code&gt; updates the row count estimate. It will no longer think you
have 100,000 rows but 20,000. But it does not shrink relpages, because that
reflects the physical file size on disk. &lt;code&gt;ANALYZE&lt;&#x2F;code&gt; can&#x27;t change that.&lt;&#x2F;p&gt;
&lt;p&gt;The planner now has accurate row estimates but wildly inaccurate page estimates.
The useful data is packed into just ~57 pages worth of entries, but the planner
doesn&#x27;t know that.&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;cost = random_page_cost × pages + cpu_index_tuple_cost × tuples ([visible in EXPLAIN output](&#x2F;posts&#x2F;explain-buffers&#x2F;))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;With a bloated index:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;pages are oversized (276 instead of ~57)&lt;&#x2F;li&gt;
&lt;li&gt;The per-page cost gets multiplied by empty pages&lt;&#x2F;li&gt;
&lt;li&gt;Total estimated cost is artificially high&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;the-hollow-index&quot;&gt;The hollow index&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-hollow-index&quot; aria-label=&quot;Anchor link for: the-hollow-index&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;We can dig even more into the index problem when we look at internal 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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; pgstatindex(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;demo_pkey&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;-[ RECORD 1 ]------+--------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;version            | 4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;tree_level         | 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;index_size         | 2260992&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;root_block_no      | 3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;internal_pages     | 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;leaf_pages         | 57&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;empty_pages        | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;deleted_pages      | 217&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;avg_leaf_density   | 86.37&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;leaf_fragmentation | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Wait, what? The avg_leaf_density is 86% and it looks perfectly healthy. That&#x27;s a
trap. Due to the hollow index (we removed 80% right in the middle) we have 57
well-packed leaf pages, but the index still contains 217 deleted pages.&lt;&#x2F;p&gt;
&lt;p&gt;This is why &lt;code&gt;avg_leaf_density&lt;&#x2F;code&gt; alone is misleading. The density of used pages
looks great, but 79% of your index file is dead weight.&lt;&#x2F;p&gt;
&lt;p&gt;The simplest way to spot index bloat is comparing actual size to expected 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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;relname&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; index_name,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pg_size_pretty(pg_relation_size(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;oid&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; actual_size,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pg_size_pretty((&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;reltuples&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 40&lt;&#x2F;span&gt;&lt;span&gt;)::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;bigint&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; expected_size,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    round&lt;&#x2F;span&gt;&lt;span&gt;((pg_relation_size(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;oid&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; nullif&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;reltuples&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 40&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;numeric&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: #F97583;&quot;&gt;as&lt;&#x2F;span&gt;&lt;span&gt; bloat_ratio&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 c&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;JOIN&lt;&#x2F;span&gt;&lt;span&gt; pg_index i &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;oid&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; i&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;indexrelid&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;relkind&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;i&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: #F97583;&quot;&gt;  AND&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;reltuples&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; 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  AND&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;relname&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; NOT LIKE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;pg_%&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  AND&lt;&#x2F;span&gt;&lt;span&gt; pg_relation_size(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;oid&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; 1024&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1024&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  -- only indexes &amp;gt; 1 MB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ORDER BY&lt;&#x2F;span&gt;&lt;span&gt; bloat_ratio &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DESC NULLS LAST&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;index_name | actual_size | expected_size | bloat_ratio&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;demo_pkey  | 2208 kB     | 781 kB        |         2.8&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;A &lt;code&gt;bloat_ratio&lt;&#x2F;code&gt; of 2.8 means the index is nearly 3x larger than expected. Anything
above 1.8 - 2.0 deserves investigation.&lt;&#x2F;p&gt;
&lt;p&gt;We filter to indexes over 1 MB - bloat on tiny indexes doesn&#x27;t matter that much.
Please, adjust the threshold based on your environment; for large databases, you
might only care about indexes over 100 MB.&lt;&#x2F;p&gt;
&lt;p&gt;But here comes &lt;strong&gt;BIG WARNING&lt;&#x2F;strong&gt;: pgstatindex() we used earlier physically reads
the entire index. On a 10 GB index, that&#x27;s 10 GB of I&#x2F;O. Don&#x27;t run it against
all indexes on a production server - unless you know what you are doing!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;reindex&quot;&gt;REINDEX&lt;a class=&quot;zola-anchor&quot; href=&quot;#reindex&quot; aria-label=&quot;Anchor link for: reindex&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;How to actually fix index bloat problem? &lt;code&gt;REINDEX&lt;&#x2F;code&gt; is s straightforward solution as
it rebuilds the index from scratch.&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;REINDEX &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INDEX&lt;&#x2F;span&gt;&lt;span&gt; CONCURRENTLY demo_pkey ;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;After which we can check the index health:&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; pgstatindex(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;demo_pkey&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;-[ RECORD 1 ]------+-------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;version            | 4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;tree_level         | 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;index_size         | 466944&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;root_block_no      | 3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;internal_pages     | 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;leaf_pages         | 55&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;empty_pages        | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;deleted_pages      | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;avg_leaf_density   | 89.5&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;leaf_fragmentation | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    relname,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pg_size_pretty(pg_relation_size(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;oid&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; file_size,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pg_size_pretty((pgstattuple(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;oid&lt;&#x2F;span&gt;&lt;span&gt;)).tuple_len) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;as&lt;&#x2F;span&gt;&lt;span&gt; actual_data&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&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;IN&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;demo&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;demo_pkey&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  | file_size | actual_data&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;demo      | 7472 kB   | 1278 kB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;demo_pkey | 456 kB    | 313 kB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Our index shrunk from 2.2 MB to 456 KB - 79% reduction (not a big surprise
though).&lt;&#x2F;p&gt;
&lt;p&gt;As you might have noticed we have used &lt;code&gt;CONCURRENTLY&lt;&#x2F;code&gt; to avoid using ACCESS
EXCLUSIVE lock. This is available since PostgreSQL 12+, and while there&#x27;s an
option to omit it - the pretty much only reason to do so is during planned
maintenance to speed up the index rebuild time.&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;If you look above at the file_size of our relations, we have managed to reclaim
the disk space for the affected index (it was &lt;code&gt;REINDEX&lt;&#x2F;code&gt; after all), but the table
space was not returned back to the operating system.&lt;&#x2F;p&gt;
&lt;p&gt;That&#x27;s where &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;cybertec-postgresql&#x2F;pg_squeeze&quot;&gt;pg_squeeze&lt;&#x2F;a&gt;
shines. Unlike trigger-based alternatives, pg_squeeze uses logical decoding,
resulting in lower impact on your running system. It rebuilds both the table and
all its indexes online, with minimal locking:&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_squeeze;&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;SELECT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; squeeze&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;squeeze_table&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;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;demo&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;The exclusive lock is only needed during the final swap phase, and its duration
can be configured. Even better, pg_squeeze is designed for regular automated
processing - you can register tables and let it handle maintenance whenever bloat
thresholds are met.&lt;&#x2F;p&gt;
&lt;p&gt;pg_squeeze makes sense when both table and indexes are bloated, or when you want
automated management. REINDEX CONCURRENTLY is simpler when only indexes need
work.&lt;&#x2F;p&gt;
&lt;p&gt;There&#x27;s also older tool pg_repack - for a deeper comparison of bloat-busting
tools, see article &lt;a href=&quot;&#x2F;posts&#x2F;the-bloat-busters-pg-repack-pg-squeeze&#x2F;&quot;&gt;The Bloat Busters: pg_repack vs
pg_squeeze&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;vacuum-full-the-nuclear-option&quot;&gt;VACUUM FULL (The nuclear option)&lt;a class=&quot;zola-anchor&quot; href=&quot;#vacuum-full-the-nuclear-option&quot; aria-label=&quot;Anchor link for: vacuum-full-the-nuclear-option&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;VACUUM FULL&lt;&#x2F;code&gt; rewrites the entire table and all indexes. While it fixes
everything it comes with a big but - it requires an ACCESS EXCLUSIVE
lock - completely blocking all reads and writes for the entire duration. For a
large table, this could mean hours of downtime.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Generally avoid this in production&lt;&#x2F;strong&gt;. Use pg_squeeze instead for the same
result without the downtime.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;when-to-act-and-when-to-chill&quot;&gt;When to act, and when to chill&lt;a class=&quot;zola-anchor&quot; href=&quot;#when-to-act-and-when-to-chill&quot; aria-label=&quot;Anchor link for: when-to-act-and-when-to-chill&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Before you now go and &lt;code&gt;REINDEX&lt;&#x2F;code&gt; everything in sight, let&#x27;s talk about when index
bloat actually matters.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;B-trees expand and contract with your data&lt;&#x2F;strong&gt;. With random insertions affecting
index columns - UUIDs, hash keys, etc. the page splits happen constantly. Index
efficiency might get hit at occassion and also settle around 70 - 80% over
different natural cycles of your system usage. That&#x27;s not bloat. That&#x27;s the tree
finding its natural shape for your data.&lt;&#x2F;p&gt;
&lt;p&gt;The bloat we demonstrated - 57 useful pages drowning in 217 deleted ones - is
extreme. It came from deleting 80% of contiguous data. You won&#x27;t see this
from normal day to day operations.&lt;&#x2F;p&gt;
&lt;p&gt;When do you need to act immediately:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;after a massive DELETE (retention policy, GDPR purge, failed migration cleanup)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;bloat_ratio&lt;&#x2F;code&gt; exceeds 2.0 and keeps climbing&lt;&#x2F;li&gt;
&lt;li&gt;query plans suddenly prefer sequential scans on indexed columns&lt;&#x2F;li&gt;
&lt;li&gt;index size is wildly disproportionate to row count&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;But in most cases you don&#x27;t have to panic. Monitor weekly and when indexes bloat
ratio continously grow above warning levels, schedule a &lt;code&gt;REINDEX CONCURRENTLY&lt;&#x2F;code&gt;
during low traffic period.&lt;&#x2F;p&gt;
&lt;p&gt;Index bloat isn&#x27;t an emergency until it is. Know the signs, have the tools
ready, and don&#x27;t let VACUUM&#x27;s silence fool you into thinking everything&#x27;s fine.&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;VACUUM is essential for PostgreSQL. Run it. Let autovacuum do its job. But
understand its limitations: it cleans up dead tuples, not index structure.&lt;&#x2F;p&gt;
&lt;p&gt;The truth about PostgreSQL maintenance is that VACUUM handles heap bloat
reasonably well, but index bloat requires explicit intervention. Know when your
indexes are actually sick versus just breathing normally - and when to reach for
REINDEX.&lt;&#x2F;p&gt;
&lt;p&gt;VACUUM handles heap bloat. Index bloat is your problem. Know the difference.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>RegreSQL: Regression Testing for PostgreSQL Queries</title>
        <published>2025-11-13T22:47:00+00:00</published>
        <updated>2025-11-13T22:47:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/regresql-testing-queries/"/>
        <id>https://boringsql.com/posts/regresql-testing-queries/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/regresql-testing-queries/">&lt;p&gt;&lt;strong&gt;TL;DR&lt;&#x2F;strong&gt; - &lt;em&gt;RegreSQL brings PostgreSQL&#x27;s regression testing methodology to your application queries, catching both correctness bugs and performance regressions before production.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;As puzzling as it might seem, the common problem with production changes is the ever-present &quot;AHA&quot; moment when things start slowing down or crashing straight away. Testing isn&#x27;t easy as it is, but there&#x27;s a widespread practice gap when it comes to testing SQL queries. Some might pretend to &quot;fix it&quot; by using ORMs to abstract away the problem. Others treat SQL as &quot;just glue code&quot; that doesn&#x27;t deserve systematic testing. Most settle for integration tests that verify the application layer works, never actually testing whether their queries will survive the next schema change or index modification.&lt;&#x2F;p&gt;
&lt;p&gt;For PostgreSQL development itself, the project has a robust regression test suite that has been preventing disasters in core development for decades. The database itself knows how to test SQL systematically - we just don&#x27;t use those same techniques for our own queries. Enter &lt;a href=&quot;&#x2F;products&#x2F;regresql&#x2F;&quot;&gt;RegreSQL&lt;&#x2F;a&gt;, a tool originally created by Dimitri Fontaine for &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;theartofpostgresql.com&quot;&gt;&lt;em&gt;The Art of PostgreSQL&lt;&#x2F;em&gt;&lt;&#x2F;a&gt; book (which is excellent for understanding and mastering PostgreSQL as a database system), designed to bring the same regression testing framework to our application queries.&lt;&#x2F;p&gt;
&lt;p&gt;I&#x27;ve been trying to use it for some time, but due to missing features and limitations gave up several times. Until now. I decided to fork the project and spend the time needed to take it to the next level.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;a class=&quot;zola-anchor&quot; href=&quot;#introduction&quot; aria-label=&quot;Anchor link for: introduction&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The &lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;boringsql.com&#x2F;products&#x2F;regresql&#x2F;&quot;&gt;RegreSQL&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; promise starts with the biggest strength and perceived weakness of SQL queries. They are just strings. And unless you use something like &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;sqlc.dev&quot;&gt;sqlc&lt;&#x2F;a&gt; (for Go), &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;darioteixeira&#x2F;pgocaml&quot;&gt;PG&#x27;OCaml&lt;&#x2F;a&gt; or Rust&#x27;s &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;launchbadge&#x2F;sqlx&quot;&gt;SQLx&lt;&#x2F;a&gt; toolkit giving you compile-time checking, your queries are validated only when they are executed. Which in better case mean either usually slow-ish test suite or integration tests, in worst scenario only when deployed. ORMs are another possibility - completely abstracting away SQL (but more on that later).&lt;&#x2F;p&gt;
&lt;p&gt;But even with compile-time checking, you are only checking for one class of problems: schema mismatches. What about behavior changes after schema migration or performance regressions? What about understanding whether your optimization actually made things faster or just moved the problem elsewhere?&lt;&#x2F;p&gt;
&lt;p&gt;This is where RegreSQL comes in. Rather than trying to turn SQL into something else, RegreSQL embraces &quot;SQL as strings&quot; reality and applies the same testing methodology PostgreSQL itself uses: regression testing. You write (or generate - continue reading) your SQL queries, provide input data, and RegreSQL verifies that future changes don&#x27;t break those expectations.&lt;&#x2F;p&gt;
&lt;p&gt;The features don&#x27;t stop there though - it tracks performance baselines, detects common query plan regressions (like sequential scans), and gives you framework for systematic experimentation with the schema changes and query change management.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;basic-regression-testing&quot;&gt;Basic regression testing&lt;a class=&quot;zola-anchor&quot; href=&quot;#basic-regression-testing&quot; aria-label=&quot;Anchor link for: basic-regression-testing&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Enough with theory. Let&#x27;s jump in straight into the action and see what a sample run of RegreSQL looks like&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;$ regresql text&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Connecting to &amp;#39;postgres:&#x2F;&#x2F;radim:password123@192.168.139.28&#x2F;cdstore_test&amp;#39;… ✓&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&gt;Running regression tests...&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&gt;✓ album-by-artist_list-albums-by-artist.1.json (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-by-artist_list-albums-by-artist.2.json (0.00s)&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&gt;✓ album-tracks_list-tracks-by-albumid.2.json (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-tracks_list-tracks-by-albumid.1.json (0.00s)&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&gt;✓ artist_top-artists-by-album.1.json (0.00s)&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&gt;✓ genre-topn_genre-top-n.top-1.json (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ genre-topn_genre-top-n.top-3.json (0.00s)&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&gt;✓ genre-tracks_tracks-by-genre.json (0.00s)&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&gt;Results: 8 passed, 0 failed, 8 skipped (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In this example based on &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;lerocha&#x2F;chinook-database&quot;&gt;Chinook database&lt;&#x2F;a&gt; (as used originally in The Art of PostgreSQL book), RegreSQL scans the current directory (or one provided by &lt;code&gt;-C &#x2F;path&#x2F;to&#x2F;project&lt;&#x2F;code&gt;) for &lt;code&gt;*.sql&lt;&#x2F;code&gt; files and attempts to run all queries against the configured PostgreSQL connection.&lt;&#x2F;p&gt;
&lt;p&gt;The individual files can contain either single or multiple sql queries. Like following 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;-- name: top-artists-by-album&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Get the list of the N artists with the most albums&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 style=&quot;color: #79B8FF;&quot;&gt;    artist&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;name&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;    count&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;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; albums&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    artist&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    LEFT JOIN&lt;&#x2F;span&gt;&lt;span&gt; album &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;USING&lt;&#x2F;span&gt;&lt;span&gt; (artist_id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GROUP BY&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    artist&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ORDER BY&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    albums &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DESC&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LIMIT&lt;&#x2F;span&gt;&lt;span&gt; :n;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The syntax for the queries supports both positional arguments (like &lt;code&gt;$1&lt;&#x2F;code&gt; known from libpq library) or (preferred) &lt;code&gt;psql&lt;&#x2F;code&gt; style variable (&lt;code&gt;:varname&lt;&#x2F;code&gt;). The each identified query (not file) is then executed for 0..N times, based on number of predefined plans and verified to the expected results - validating the expected data matches the one returned. The support for SQL files handling is available separately with https:&#x2F;&#x2F;github.com&#x2F;boringSQL&#x2F;queries (Go version only for now).&lt;&#x2F;p&gt;
&lt;p&gt;This gives you what original RegreSQL tool has introduced - change your schema, refactor a query, run &lt;code&gt;regresql test&lt;&#x2F;code&gt; and see immediately what broke. The test suite now has ability to catch regressions before they are committed &#x2F; shipped. The current version built on top of it, giving you better console formatter instead of TAP style output, as well as jUnit, JSON and GitHub actions formatters for better integration into your CI&#x2F;CD pipelines.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;performance-regression-testing&quot;&gt;Performance regression testing&lt;a class=&quot;zola-anchor&quot; href=&quot;#performance-regression-testing&quot; aria-label=&quot;Anchor link for: performance-regression-testing&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Basic regression testing catches correctness issues - wrong results, broken queries, schema mismatches. But there&#x27;s another class of production issues it misses. Performance regressions. No matter how unbelievable it might sound but queries get deployed without appropriate indexes — which &lt;a href=&quot;&#x2F;posts&#x2F;why-postgresql-indexes-are-ignored&#x2F;&quot;&gt;might then be ignored&lt;&#x2F;a&gt; entirely — or they change over time. Simple fix - both for handwritten SQL or ORM code - can switch from milliseconds to seconds. You add index that helps one query, but tanks another. You modify conditionals and accidently force a &lt;a href=&quot;&#x2F;posts&#x2F;postgresql-statistics&quot;&gt;sequential scan of millions of rows&lt;&#x2F;a&gt;. This is where it hurts.&lt;&#x2F;p&gt;
&lt;p&gt;RegreSQL addresses this by tracking performance baselines alongside correctness. Once baselines are generated&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;$ regresql baseline&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Connecting to &amp;#39;postgres:&#x2F;&#x2F;appuser:password123@192.168.139.28&#x2F;cdstore_test&amp;#39;… ✓&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Creating baselines directory: regresql&#x2F;baselines&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Creating directory &amp;#39;regresql&#x2F;baselines&amp;#39;&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&gt;Creating baselines for queries:&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&gt;  .&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Created baseline: album-by-artist_list-albums-by-artist.1.json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Created baseline: album-by-artist_list-albums-by-artist.2.json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Created baseline: album-tracks_list-tracks-by-albumid.1.json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Created baseline: album-tracks_list-tracks-by-albumid.2.json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Created baseline: artist_top-artists-by-album.1.json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Created baseline: genre-topn_genre-top-n.top-1.json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Created baseline: genre-topn_genre-top-n.top-3.json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Created baseline: genre-tracks_tracks-by-genre.json&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&gt;Baselines have been created successfully!&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Baseline files are stored in: regresql&#x2F;baselines&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;the test command not only tests the regressions to the captured times, but also detects the common bad patterns in &lt;a href=&quot;&#x2F;posts&#x2F;explain-buffers&#x2F;&quot;&gt;query execution plans&lt;&#x2F;a&gt;. For now it provides warnings for detection of sequential scans - both on their and&#x2F;or with nested loops and multiple sort operations. I believe this alone might provide a valuable insights and reduce the mishaps in production. It&#x27;s also a place where further development of RegreSQL will take place.&lt;&#x2F;p&gt;
&lt;p&gt;To demonstrate this, let&#x27;s review the test output with the baselines.&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;Connecting to &amp;#39;postgres:&#x2F;&#x2F;appuser:password123@192.168.139.28&#x2F;cdstore_test&amp;#39;… ✓&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&gt;Running regression tests...&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&gt;✓ album-by-artist_list-albums-by-artist.1.json (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-by-artist_list-albums-by-artist.2.json (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-by-artist_list-albums-by-artist.1.cost (22.09 &amp;lt;= 22.09 * 110%) (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Sequential scan detected on table &amp;#39;artist&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Consider adding an index if this table is large or this query is frequently executed&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Nested loop join with sequential scan detected&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Add index on join column to avoid repeated sequential scans&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-by-artist_list-albums-by-artist.2.cost (22.09 &amp;lt;= 22.09 * 110%) (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Sequential scan detected on table &amp;#39;artist&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Consider adding an index if this table is large or this query is frequently executed&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Nested loop join with sequential scan detected&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Add index on join column to avoid repeated sequential scans&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&gt;✓ album-tracks_list-tracks-by-albumid.1.json (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-tracks_list-tracks-by-albumid.2.json (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-tracks_list-tracks-by-albumid.1.cost (8.23 &amp;lt;= 8.23 * 110%) (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ album-tracks_list-tracks-by-albumid.2.cost (8.23 &amp;lt;= 8.23 * 110%) (0.00s)&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&gt;✓ artist_top-artists-by-album.1.json (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ artist_top-artists-by-album.1.cost (35.70 &amp;lt;= 35.70 * 110%) (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Multiple sequential scans detected on tables: album, artist&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Review query and consider adding indexes on filtered&#x2F;joined columns&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&gt;✓ genre-topn_genre-top-n.top-1.json (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ genre-topn_genre-top-n.top-3.json (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ genre-topn_genre-top-n.top-1.cost (6610.59 &amp;lt;= 6610.59 * 110%) (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Multiple sequential scans detected on tables: genre, artist&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Review query and consider adding indexes on filtered&#x2F;joined columns&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Multiple sort operations detected (2 sorts)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Consider composite indexes for ORDER BY clauses to avoid sorting&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Nested loop join with sequential scan detected&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Add index on join column to avoid repeated sequential scans&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ genre-topn_genre-top-n.top-3.cost (6610.59 &amp;lt;= 6610.59 * 110%) (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Multiple sequential scans detected on tables: artist, genre&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Review query and consider adding indexes on filtered&#x2F;joined columns&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Multiple sort operations detected (2 sorts)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Consider composite indexes for ORDER BY clauses to avoid sorting&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Nested loop join with sequential scan detected&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Add index on join column to avoid repeated sequential scans&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&gt;✓ genre-tracks_tracks-by-genre.json (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✓ genre-tracks_tracks-by-genre.cost (37.99 &amp;lt;= 37.99 * 110%) (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ⚠️  Multiple sequential scans detected on tables: genre, track&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Suggestion: Review query and consider adding indexes on filtered&#x2F;joined columns&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&gt;Results: 16 passed (0.00s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;As you can see, despite from not having baseline, RegreSQL is able to detect the basic bad patterns that should be addressed before queries can be considered &quot;production ready&quot;.&lt;&#x2F;p&gt;
&lt;p&gt;In some cases, having the detection of sequential scans, or just tracking query costs baselines might be considered undesirable, which would lead to false positives. RegreSQL enables this to be addressed by query metadata as demonstrated below.&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;-- name: query_name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- metadata: key1=value1, key2=value2&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;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;At this point RegreSQL recognizes&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;notest&lt;&#x2F;code&gt; to skip the query testing altogether (not just cost tracking)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;nobaseline&lt;&#x2F;code&gt; to skip cost tracking&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;noseqscanwarn&lt;&#x2F;code&gt; to keep cost tracking but disable sequential scan warnings&lt;&#x2F;li&gt;
&lt;li&gt;and &lt;code&gt;difffloattolerance&lt;&#x2F;code&gt; to cost failure threshold (default 10% at the moment).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&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;-- name: query_name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- regresql: notest, nobaseline&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- regresql: noseqscanwarn&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- regresql: difffloattolerance:0.25&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- query that can vary in cost by 20% without being considered a failure&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;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;orm-enters-the-room&quot;&gt;ORM enters the room&lt;a class=&quot;zola-anchor&quot; href=&quot;#orm-enters-the-room&quot; aria-label=&quot;Anchor link for: orm-enters-the-room&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;ORMs abstract away SQL, but they still generate it - much like &lt;a href=&quot;&#x2F;posts&#x2F;view-inlining&#x2F;&quot;&gt;view inlining&lt;&#x2F;a&gt; where the planner rewrites your SQL behind the scenes - and that generated SQL can have performance problems you won&#x27;t catch until production. Consider this common scenario: you start with a simple SQLAlchemy query that works fine, then months later add eager loading for related 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;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;orders&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&gt;    session.query(Order)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    .filter(Order.user_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span&gt; user_id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    .options(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        joinedload(Order.user),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        joinedload(Order.shipping_address),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        selectinload(Order.items)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # NEW: Load order items&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;    .all()&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;That innocent &lt;code&gt;selectinload(Order.items)&lt;&#x2F;code&gt; generates a separate query - and without an index on &lt;code&gt;order_items.order_id&lt;&#x2F;code&gt;, it performs a sequential scan.&lt;&#x2F;p&gt;
&lt;p&gt;RegreSQL can catch this by intercepting ORM-generated SQL using SQLAlchemy&#x27;s event system:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@event.listens_for&lt;&#x2F;span&gt;&lt;span&gt;(engine,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;before_cursor_execute&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: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; capture_sql&lt;&#x2F;span&gt;&lt;span&gt;(conn, cursor, statement,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span&gt;args):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    captured_queries.append(statement)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Run your ORM code, capture the SQL, save it as a .sql file, and test it with RegreSQL. The performance baseline testing will flag the missing index before it hits production. This is currently experimental, but ORM integration is a key area for RegreSQL&#x27;s future development.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;test-data-management&quot;&gt;Test Data Management&lt;a class=&quot;zola-anchor&quot; href=&quot;#test-data-management&quot; aria-label=&quot;Anchor link for: test-data-management&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Up until now we have covered how RegreSQL verifies query correctness and tracks performance regressions. But there&#x27;s a critical prerequisite we&#x27;ve only skimmed through.  Every regression test needs consistent, reproducible data. Change the data, change their cardinality, and your expected results become meaningless. Your performance  baselines drift. Your tests become flaky.&lt;&#x2F;p&gt;
&lt;p&gt;Traditional approach to create test data might involve&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Database dumps&lt;&#x2F;strong&gt; become unmanageable - 500MB files you can&#x27;t review, can&#x27;t understand, that break with every schema migration, and whose data becomes stale as production evolves. Which version of the dump are your tests even using?&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;SQL scripts&lt;&#x2F;strong&gt; might be better than dumps, but still imperative and hard to maintain. You end up with INSERT statements scattered across multiple files, managing foreign keys manually, and debugging constraint violations.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Factories in application code&lt;&#x2F;strong&gt; might work great for integration tests, but we&#x27;re testing SQL directly. Do you really want to maintain parallel data generation in your application language just for SQL tests?&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Shared test database&lt;&#x2F;strong&gt; is the synonym for classic &quot;works on my machine&quot; problem. State leaks between tests. Parallel execution becomes impossible. Debugging is a nightmare.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;What we need is something that&#x27;s declarative (what data, not how to insert it), reproducible (similar data every time), composable (build complex scenarios from simple pieces), and scalable (from 10 rows to 100,000).&lt;&#x2F;p&gt;
&lt;p&gt;This is where next improvement in RegreSQL&#x27;s fixture system comes in. Think of it as infrastructure-as-code for your test data. You describe the data you need in YAML files, and RegreSQL handles the rest - dependencies, cleanup, foreign keys, and even realistic data generation at scale.&lt;&#x2F;p&gt;
&lt;p&gt;RegreSQL&#x27;s fixture system lets you define test data in YAML files stored in &lt;code&gt;regresql&#x2F;fixtures&#x2F;&lt;&#x2F;code&gt;. Here&#x27;s a simple 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;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  fixture&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; basic_users&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  description&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; a handful of test users&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  cleanup&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; rollback&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: #85E89D;&quot;&gt;  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;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; table&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; users&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      rows&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: #85E89D;&quot;&gt; id&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          email&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; alice@example.com&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; Alice Anderson&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          created_at&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2024-01-15&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: #85E89D;&quot;&gt; id&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          email&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; bob@example.com&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; Bob Builder&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          created_at&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2024-02-20&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;To use this fixture in your tests, reference it in the query&#x27;s plan file (&lt;code&gt;regresql&#x2F;plans&#x2F;get-user.yaml&lt;&#x2F;code&gt;) you can just reference the fixture&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  fixtures&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: #9ECBFF;&quot;&gt; basic_users&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: #9ECBFF;&quot;&gt;  &amp;quot;1&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: #85E89D;&quot;&gt;    email&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; alice@example.com&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: #9ECBFF;&quot;&gt;  &amp;quot;2&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: #85E89D;&quot;&gt;    email&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; bob@example.com&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And when you run &lt;code&gt;regresql test&lt;&#x2F;code&gt;, the fixture is automatically loaded before the query executes, and cleaned up afterward. No manual setup scripts, no state leakage between tests. But it does not stop with static fixtures. When you want to test queries against realistic volumes you can use range of &lt;strong&gt;data generators&lt;&#x2F;strong&gt; including&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;sequences, random integer, decimal, string, uuid, email and name generators&lt;&#x2F;li&gt;
&lt;li&gt;date_between for generating random timestamps within a range&lt;&#x2F;li&gt;
&lt;li&gt;foreign key references to be able to reuse data from other table&#x27;s fixtures&lt;&#x2F;li&gt;
&lt;li&gt;range to select value from predefined sources&lt;&#x2F;li&gt;
&lt;li&gt;Go template support&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; fixture&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; realistic_orders&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  generate&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: #85E89D;&quot;&gt; table&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; customers&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      count&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      columns&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: #85E89D;&quot;&gt;        id&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: #85E89D;&quot;&gt;          generator&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sequence&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          start&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        email&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: #85E89D;&quot;&gt;          generator&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; email&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          domain&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; shop.example.com&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        name&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: #85E89D;&quot;&gt;          generator&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          type&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; full&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        created_at&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: #85E89D;&quot;&gt;          generator&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; date_between&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          start&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;2023-01-01&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          end&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;2024-12-31&amp;quot;&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&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; table&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; orders&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      count&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 5000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      columns&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: #85E89D;&quot;&gt;        id&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: #85E89D;&quot;&gt;          generator&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sequence&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          start&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        customer_id&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: #85E89D;&quot;&gt;          generator&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; int&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          min&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          max&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        amount&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: #85E89D;&quot;&gt;          generator&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; decimal&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          min&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 10.00&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          max&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 999.99&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          precision&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        order_date&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: #85E89D;&quot;&gt;          generator&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; date_between&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          start&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;2023-01-01&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          end&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;2024-12-31&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This generates 1,000 customers and 5,000 orders with realistic-looking data - names, emails, dates, and amounts that feel production-like.&lt;&#x2F;p&gt;
&lt;p&gt;The fixtures are also &lt;strong&gt;stackable&lt;&#x2F;strong&gt; and can be build on top of each other. For example if you need to make sure users fixtures are created before orders fixtures, just declare the dependency (the already planned improvement is to include the support automatic foreign-key detection to avoid ID hard-coding). RegreSQL loads fixtures in dependency order and handles cleanup in reverse.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  fixture&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; orders_with_shipping&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  depends_on&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: #9ECBFF;&quot;&gt; basic_users&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: #85E89D;&quot;&gt;  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;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; table&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; orders&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      rows&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: #85E89D;&quot;&gt; id&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 101&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          user_id&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 style=&quot;color: #6A737D;&quot;&gt;  # References Alice from basic_users&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          total&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 99.99&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          status&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; shipped&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Should the available options for fixtures (manual data or data generators) not be enough, you always have options to use good old SQL based data generation.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  fixture&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; mixed_setup&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  description&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; Combine SQL with YAML and generated data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  cleanup&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; rollback&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;  # SQL executes first (either as file or inline)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  sql&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: #85E89D;&quot;&gt; file&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sql&#x2F;setup_schema.sql&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: #85E89D;&quot;&gt; inline&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;INSERT INTO config (key, value) VALUES (&amp;#39;version&amp;#39;, &amp;#39;1.0&amp;#39;);&amp;quot;&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;  # followed YAML data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  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;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; table&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; users&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      rows&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: #85E89D;&quot;&gt; id&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          email&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; admin@example.com&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;  # and finally generated data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  generate&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: #85E89D;&quot;&gt; table&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; orders&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      count&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 100&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      columns&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: #85E89D;&quot;&gt;        id&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: #85E89D;&quot;&gt;          generator&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sequence&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          start&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        user_id&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: #85E89D;&quot;&gt;          generator&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; int&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          min&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          max&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;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;RegreSQL provides commands to inspect and validate your fixtures&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;  # List all available fixtures&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  regresql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; fixtures list&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;  # Show fixture details and dependencies&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  regresql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; fixtures show realistic_orders&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;  # Validate fixture definitions&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  regresql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; fixtures validate&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;  # Show dependency graph&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  regresql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; fixtures deps&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;  # Apply fixture manually (for debugging)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  regresql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; fixtures apply basic_users&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The fixture system has been design to transforms test data from a maintenance burden into a documented, version-controlled process. Your YAML files become the single source of truth for what data your tests need, making it easy to understand test scenarios and maintain test data as the application evolves.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;(EDIT 2025-11-20)&lt;&#x2F;strong&gt; The dynamically generated fixtures on each tests will break the correctness testing of
RegreSQL. You have to use fixtures according to your use&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Fixed generated fixtures re-used between tests&lt;&#x2F;li&gt;
&lt;li&gt;Use RegreSQL to test fixtures before&#x2F;after migration&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;I&#x27;m working on next release of RegreSQL to address DX of fixtures usage.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;regresql-future&quot;&gt;RegreSQL future&lt;a class=&quot;zola-anchor&quot; href=&quot;#regresql-future&quot; aria-label=&quot;Anchor link for: regresql-future&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Introducing a new open source project is an ambitious goal, and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;boringsql.com&#x2F;products&#x2F;regresql&#x2F;&quot;&gt;RegreSQL&lt;&#x2F;a&gt; is just starting up. Despite the fork being in works for almost 2 years. In coming weeks and months I plan further improvements, as well as better documentation and more tutorials. The project is maintained as part of my &lt;strong&gt;boringSQL&lt;&#x2F;strong&gt; brand, where it&#x27;s vital component (together with pgTap) for building &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;labs.boringsql.com&quot;&gt;SQL Labs&lt;&#x2F;a&gt; which (as I sincerely hope) will provide a foundation for its further development.&lt;&#x2F;p&gt;
&lt;p&gt;At the same time &lt;strong&gt;RegreSQL&lt;&#x2F;strong&gt; is an attempt to give back to welcoming PostgreSQL community, make developer user experience slightly better if possible and (just maybe) provide one more argument against the case that SQL queries are not testable.&lt;&#x2F;p&gt;
&lt;p&gt;RegreSQL is available at &lt;a href=&quot;&#x2F;products&#x2F;regresql&#x2F;&quot;&gt;boringsql.com&#x2F;products&#x2F;regresql&lt;&#x2F;a&gt; - feel free to open issue, or drop me email about the project at &lt;a href=&quot;mailto:radim@boringsql.com&quot;&gt;radim@boringsql.com&lt;&#x2F;a&gt; or connect on &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.linkedin.com&#x2F;in&#x2F;1radim&#x2F;&quot;&gt;LinkedIn&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Beyond Start and End: PostgreSQL Range Types</title>
        <published>2025-11-02T21:40:00+00:00</published>
        <updated>2025-11-02T21:40:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/beyond-start-end-columns/"/>
        <id>https://boringsql.com/posts/beyond-start-end-columns/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/beyond-start-end-columns/">&lt;p&gt;One of the most read articles at boringSQL is &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;boringsql.com&#x2F;posts&#x2F;know-the-time-in-postgresql&#x2F;&quot;&gt;Time to Better Know The Time in PostgreSQL&lt;&#x2F;a&gt; where we dived into the complexities of storing and handling time operations in PostgreSQL. While the article introduced the range data types, there&#x27;s so much more to them. And not only for handling time ranges. In this article we will cover why to consider range types and how to work with them.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bug-not-invented-here&quot;&gt;Bug Not Invented Here&lt;a class=&quot;zola-anchor&quot; href=&quot;#bug-not-invented-here&quot; aria-label=&quot;Anchor link for: bug-not-invented-here&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;But before we can talk about the range types, let&#x27;s try to understand why we should look at them in the first place. Let&#x27;s imagine a booking platform for large flash sales of the seats, that goes live at 10pm and will be taken by storm by thousands of people who want to get their tickets.&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; seat_holds&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;    hold_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BIGINT 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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    seat_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INTEGER NOT NULL REFERENCES&lt;&#x2F;span&gt;&lt;span&gt; seats(id),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    user_session_id UUID &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: #6A737D;&quot;&gt;    -- define the hold period explicitly&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    hold_started_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TIMESTAMPTZ NOT NULL DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; CURRENT_TIMESTAMP,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    hold_expires_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TIMESTAMPTZ 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;    created_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TIMESTAMPTZ NOT NULL DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; CURRENT_TIMESTAMP&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;CREATE INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; seat_holds_on_seat_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; seat_holds(seat_id);&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; seat_holds_on_expiration&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; seat_holds(hold_expires_at);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;While the table design looks perfectly reasonable, it has one &lt;strong&gt;serious flaw&lt;&#x2F;strong&gt; - there&#x27;s no database-level atomicity guarantee there to prevent two holds for the same &lt;code&gt;seat_id&lt;&#x2F;code&gt; at the same time. The table design requires on application logic to check for the existing holds before inserting a new hold, and at the same time it does not provide any high-concurrency guarantee.&lt;&#x2F;p&gt;
&lt;p&gt;If all you have in your toolbelt are those two columns you will end up increasing the complexity to make it work. You started with one problem and soon your application developers might want to ask you to add caching layer (most likely external K&#x2F;V store) to place the holds there and very soon you have N-problems when you will resort to building a complex, custom application-side locking mechanism that is bug-prone and difficult to maintain.&lt;&#x2F;p&gt;
&lt;p&gt;Other possibility is to bring all the operations on seat holds into more complex transaction management. Which is literally invitation for disaster in &lt;strong&gt;extreme contention&lt;&#x2F;strong&gt; situation like flash ticket sales. No matter which blocking strategy you use &lt;code&gt;SERIALIZABLE&lt;&#x2F;code&gt; transaction isoliation or pessimistic locking using &lt;code&gt;SELECT ... FOR UPDATE&lt;&#x2F;code&gt; will create a large overhead in application logic (retries, massive contention on database resources, etc.).&lt;&#x2F;p&gt;
&lt;p&gt;And as we are going to talk about range data types, there&#x27;s a first possible option to solve the problem directly on database level.&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;-- needed to add GiST support to equality of integers (for seat_id)&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&lt;&#x2F;span&gt;&lt;span&gt; EXTENSION &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IF NOT EXISTS&lt;&#x2F;span&gt;&lt;span&gt; btree_gist;&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;ALTER TABLE&lt;&#x2F;span&gt;&lt;span&gt; seat_holds &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ADD CONSTRAINT&lt;&#x2F;span&gt;&lt;span&gt; seat_holds_no_overlap&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    EXCLUDE &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;USING&lt;&#x2F;span&gt;&lt;span&gt; gist (&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        seat_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WITH =&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;        tsrange(hold_started_at, hold_expires_at) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WITH&lt;&#x2F;span&gt;&lt;span&gt; &amp;amp;&amp;amp;&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;Which will add the constraint directly on the table level and enable atomic conflict detection for your seat holds with minimum locking overhead. This guarantees that the database will never allow two overlapping holds to exist for the same seat, irrespective of how many concurrent users are attempting to book. The real win here is data integrity - you&#x27;re making it impossible to have invalid state in your database, not just unlikely. While you&#x27;ll still need retry logic in your application when conflicts occur, you&#x27;ve moved the correctness guarantee from application code (where bugs hide) into the database schema (where it&#x27;s enforced). The GiST (Generalized Search Tree) index is the crucial component which makes checking for overlapping time ranges effective even under extreme load.&lt;&#x2F;p&gt;
&lt;p&gt;But really, if you look at the proposed fix, it&#x27;s still a workaround - we&#x27;re converting two separate &lt;code&gt;TIMESTAMPTZ&lt;&#x2F;code&gt; columns into a range type on the fly, when range types already include native GiST support out of the box.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;introducing-the-data-range-types&quot;&gt;Introducing the Data Range Types&lt;a class=&quot;zola-anchor&quot; href=&quot;#introducing-the-data-range-types&quot; aria-label=&quot;Anchor link for: introducing-the-data-range-types&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;You’ve seen the power of the &lt;code&gt;EXCLUDE&lt;&#x2F;code&gt; constraint to solve the concurrency problem, but why to settle for workaround (unless it&#x27;s temporary as part of the bigger refactoring) instead of going all the way in?&lt;&#x2F;p&gt;
&lt;p&gt;This brings us to the core of the matter: &lt;strong&gt;PostgreSQL&#x27;s native Range Types&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL provides a set of built-in range types, all following the pattern of type and range:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int4range&lt;&#x2F;code&gt; for integer&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;int8range&lt;&#x2F;code&gt; for bigint&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;numrange&lt;&#x2F;code&gt; for numeric&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;tsrange&lt;&#x2F;code&gt; for timestamp without time zone&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;tstzrange&lt;&#x2F;code&gt; for timestamp with time zone (which we briefly saw above)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;daterange&lt;&#x2F;code&gt; for date&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;And it does not stop there. You can easily define your &lt;strong&gt;own custom range types&lt;&#x2F;strong&gt; over any basic data type.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;first-win-cleaner-schema&quot;&gt;First win: cleaner schema&lt;a class=&quot;zola-anchor&quot; href=&quot;#first-win-cleaner-schema&quot; aria-label=&quot;Anchor link for: first-win-cleaner-schema&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;When using &lt;code&gt;start&lt;&#x2F;code&gt; and &lt;code&gt;end&lt;&#x2F;code&gt; columns you are not explicitely telling the database that these two columns are single concept representing time span. The logic to work with those two columns resides only in your queries and application code.&lt;&#x2F;p&gt;
&lt;p&gt;When you refactor our sample table to embrace the native range type, it becomes more expressive and inherently correct.  The application code no longer needs to manage two separate boundaries.&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; seat_holds_native&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;    hold_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BIGINT 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;    seat_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INTEGER NOT NULL REFERENCES&lt;&#x2F;span&gt;&lt;span&gt; seats(id),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    user_session_id UUID &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&gt;    hold_period TSTZRANGE &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&gt;    created_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TIMESTAMPTZ NOT NULL DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; CURRENT_TIMESTAMP&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 is the power of a &lt;strong&gt;first-class data type&lt;&#x2F;strong&gt;. We&#x27;ve shifted the burden from the application logic to the database schema, making the table definition itself communicate its intent more clearly.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;second-win-atomicity-guaranteed&quot;&gt;Second win: Atomicity guaranteed&lt;a class=&quot;zola-anchor&quot; href=&quot;#second-win-atomicity-guaranteed&quot; aria-label=&quot;Anchor link for: second-win-atomicity-guaranteed&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;While the new schema is cleaner, the real database win comes from enforcing our concurrency guarantee - preventing two seats from being double-booked. To achieve this you can reuse the &lt;strong&gt;exclusion constraint&lt;&#x2F;strong&gt; as demonstrated previously.&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 TABLE&lt;&#x2F;span&gt;&lt;span&gt; seat_holds_native &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ADD CONSTRAINT&lt;&#x2F;span&gt;&lt;span&gt; seat_holds_no_overlap&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    EXCLUDE &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;USING&lt;&#x2F;span&gt;&lt;span&gt; gist (&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        seat_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WITH =&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;        hold_period &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WITH&lt;&#x2F;span&gt;&lt;span&gt; &amp;amp;&amp;amp;&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 time you can use &lt;code&gt;hold_period&lt;&#x2F;code&gt; directly without need to explicitely convert it. This constraint enforces two rules at once:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;seat_id WITH =&lt;&#x2F;code&gt; ensures the constraint only applies to holds for the same seat.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;hold_period WITH &amp;amp;&amp;amp;&lt;&#x2F;code&gt; checking the overlap of hold periods with the operator &lt;code&gt;&amp;amp;&amp;amp;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Finally &lt;code&gt;EXCLUDE USING gist&lt;&#x2F;code&gt;  is the crucial technical detail, telling PostgreSQL to use GiST index to enforce the constraint.  This is not specific to range types, as &lt;code&gt;EXCLUDE&lt;&#x2F;code&gt; constraint can&#x27;t exist without an index to enforce it (common use cases might include arrays, geometric data, etc.).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;range-boundaries-a-quick-math-refresher&quot;&gt;Range boundaries: A quick math refresher&lt;a class=&quot;zola-anchor&quot; href=&quot;#range-boundaries-a-quick-math-refresher&quot; aria-label=&quot;Anchor link for: range-boundaries-a-quick-math-refresher&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Before we dive into the operators, let&#x27;s take a moment to understand how PostgreSQL represents range boundaries. If you remember your high school math, range types use the same notation as intervals in mathematics.&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL ranges can have four different boundary types:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Inclusive boundaries -&lt;&#x2F;strong&gt; &lt;code&gt;[&lt;&#x2F;code&gt; and &lt;code&gt;]&lt;&#x2F;code&gt; An inclusive boundary includes the endpoint value in the range. Think of it as &quot;less than or equal to&quot; or &quot;greater than or equal to&quot;.&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;-- [10, 20] includes both 10 and 20&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- represents 10 ≤ x ≤ 20&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; int4range(&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;20&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[]&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;&lt;strong&gt;Exclusive boundaries -&lt;&#x2F;strong&gt; &lt;code&gt;(&lt;&#x2F;code&gt; and &lt;code&gt;)&lt;&#x2F;code&gt; An exclusive boundary excludes the endpoint value from the range. This is &quot;less than&quot; or &quot;greater than&quot; without the equality.&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;-- (10, 20) excludes both 10 and 20&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- represents 10 &amp;lt; x &amp;lt; 20&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; int4range(&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;20&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;()&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;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Mixed boundaries -&lt;&#x2F;strong&gt; &lt;code&gt;[)&lt;&#x2F;code&gt; or &lt;code&gt;(]&lt;&#x2F;code&gt; You can mix and match. The most common and &lt;strong&gt;default pattern&lt;&#x2F;strong&gt; in PostgreSQL is &lt;code&gt;[)&lt;&#x2F;code&gt; - inclusive lower bound, exclusive upper bound. This is particularly useful for timestamps and dates because it naturally represents &quot;from the start of one period up to (but not including) the start of the next&quot;.&lt;&#x2F;p&gt;
&lt;p&gt;As mentioned, the default boundary &lt;code&gt;[)&lt;&#x2F;code&gt; eliminates the natural ambiguity when representing consecutive periods.&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;-- these ranges are adjacent, not overlapping&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Week 1&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 style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;[2025-11-01, 2025-11-08)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Week 2&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 style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;[2025-11-08, 2025-11-15)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;With this notation, the end of one period is exactly the start of the next, with no gaps or overlaps. This makes it perfect for time-based ranges, inventory availability windows, or any scenario where you&#x27;re dividing a continuum into distinct segments.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;canonicalization-and-range-set-operations&quot;&gt;Canonicalization and Range Set Operations&lt;a class=&quot;zola-anchor&quot; href=&quot;#canonicalization-and-range-set-operations&quot; aria-label=&quot;Anchor link for: canonicalization-and-range-set-operations&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Boundaries are not the only aspect of the ranges that behave like mathematical sets, allowing arithmetic operations and canonicalization of the discrete ranges.&lt;&#x2F;p&gt;
&lt;p&gt;For &lt;strong&gt;discrete&lt;&#x2F;strong&gt; range types (int4range, int8range, daterange), multiple representations can actually mean the exact same set of values. For example, for integers, the range [10, 20] (inclusive on both ends) is the same set as (9, 21) (exclusive on both ends) or the default PostgreSQL canonical form [10, 21) (inclusive lower, exclusive upper).&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL uses a &lt;strong&gt;canonicalization function&lt;&#x2F;strong&gt; to convert all equivalent discrete ranges into a single, uniform representation (default &lt;code&gt;[)&lt;&#x2F;code&gt; boundary mentioned above), which is essential for accurate equality checks and indexing.&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;-- [10, 20] includes integers 10, 11, ..., 20.&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; int4range(&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;20&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[]&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; original_range;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Result: [10,21)&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;-- (9, 21) includes integers 10, 11, ..., 20.&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; int4range(&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: #79B8FF;&quot;&gt;21&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;()&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; original_range;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Result: [10,21)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Exception are &lt;strong&gt;continuous ranges&lt;&#x2F;strong&gt; (think floats and timestamps with fractional seconds) where PostgreSQL won&#x27;t use canonicalization because a boundary change always means a change in the contained values as there&#x27;s no easy to define &quot;next value&quot;. I.e. there&#x27;s no next value for 20.0 (i.e. not 20.0001, nor 20.000001, etc.) and changing boundary would change it&#x27;s meaning.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-operator-toolkit&quot;&gt;The operator toolkit&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-operator-toolkit&quot; aria-label=&quot;Anchor link for: the-operator-toolkit&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The type ranges and by their definition GiST (in this instance range_ops) and GIN (array_ops) indexes come with number of operator that makes your life easier.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Overlap operator - &lt;code&gt;&amp;amp;&amp;amp;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;
As mentioned already above the overlap operator is the most fundamental one. It simply checks whether two ranges share any common data points.&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;-- find holds active at any point between 10:00 and 11:00&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; seat_holds_native &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; hold_period &amp;amp;&amp;amp; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[2025-12-25 10:00, 2025-12-25 11:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Contains operator - &lt;code&gt;@&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;
For checking against specific moments in time, we might turn to the Contains operator. It verifies whatever the range on the left completely containts the element on the right (which might be both underlying data type or range type).&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;-- find holds that are active at the specific momement&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; seat_holds_native &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; hold_period @&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;2025-11-05 15:00&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz&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;-- find holds that are active at the specific time range&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; seat_holds_native &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; hold_period @&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;[2025-12-25 10:00, 2025-12-25 10:15)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Contained By operator - &lt;code&gt;&amp;lt;@&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt;
In contrast, the Contained By operator checks the reverse relationship - whatever the range on the left is entirely contained by the range on the right.&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;-- find holds that are within November &amp;#39;25&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; seat_holds_native &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; hold_period &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span&gt;@ &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[2025-11-01, 2025-12-01)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Strictly Before&#x2F;After operators - &lt;code&gt;&amp;lt;&amp;lt;&lt;&#x2F;code&gt; and &lt;code&gt;&amp;gt;&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; operators allow you to query ranges that are completely separated from the reference range (i.e. don&#x27;t even touch the boundaries).&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;-- find holds that finished strictly before 10 November&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SELECT * FROM seat_holds_native WHERE hold_period &amp;lt;&amp;lt; &amp;#39;[2025-11-10, 2025-11-15)&amp;#39;::tstzrange;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Boundary Extension operators - &lt;code&gt;&amp;amp;&amp;lt;&lt;&#x2F;code&gt; and &lt;code&gt;&amp;amp;&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; let you reason about range boundaries independently, checking whether one range extends beyond another&#x27;s endpoints (i.e. it can start&#x2F;end anywhere within the given range).&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;-- find holds that end before or at the same time as reference range ends&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; seat_holds_native &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; hold_period &amp;amp;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;[2025-11-08 17:00, 2025-11-08 18:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange;&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;-- find holds that start at or after reference range starts&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; seat_holds_native &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; hold_period &amp;amp;&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;[2025-11-08 09:00, 2025-11-08 18:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Finally the &lt;strong&gt;Adjecent operator - &lt;code&gt;-|-&lt;&#x2F;code&gt;&lt;&#x2F;strong&gt; checks if two ranges are perfectly contiguous - they MUST touch at exactly one boundary point, but do not overlap. This might be invaluable when checking if a customer can extend an existing hold without any gap or conflict.&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;-- find holds that are immediately adjacent (touching) to given range&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; seat_holds_native &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; hold_period &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&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: #9ECBFF;&quot;&gt; &amp;#39;[2025-11-08 17:00, 2025-11-08 18:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;to-infinity-and-beyond&quot;&gt;To Infinity and Beyond&lt;a class=&quot;zola-anchor&quot; href=&quot;#to-infinity-and-beyond&quot; aria-label=&quot;Anchor link for: to-infinity-and-beyond&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Similar to base types ranges in PostgreSQL can handle NULL values, but it does not stop there. There are also special states specifically applicable to data type ranges: &lt;code&gt;empty&lt;&#x2F;code&gt; and &lt;code&gt;infinity&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s start with &lt;strong&gt;infinite bounds&lt;&#x2F;strong&gt;, the bound that shows the real power of the ranges. You can define range that extend infinitely in either direction (or both 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;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- range that never expires (upper bound is infinite)&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 style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;[2025-11-01 10:00, infinity)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange;&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;-- range that has always been valid (lower bound is infinite)&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 style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;[-infinity, 2025-11-01 10:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange;&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;-- range covering all time&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 style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;[-infinity, infinity)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This gives you ability to describe the &quot;from this points forward&quot; use cases. As we will cover later we can easily define lifetime subscription.&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;-- lifetime subscription that never expires&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; subscriptions (user_id, plan, active_period)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;42&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;lifetime&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[2025-11-01, infinity)&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- all active subscriptions right now&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; subscriptions&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; active_period @&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt; NOW&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;Using &lt;code&gt;infinity&lt;&#x2F;code&gt; is far more elegant solution that using NULL values or &quot;special&quot; values like &lt;code&gt;2099-31-12&lt;&#x2F;code&gt; - it&#x27;s explicit and clearly communicates the data intent.&lt;&#x2F;p&gt;
&lt;p&gt;At any point you can validate whatever range has infinite bounds:&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  lower_inf(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[2025-11-01, infinity)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; lower_is_infinite,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  upper_inf(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[2025-11-01, infinity)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; upper_is_infinite;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;understanding-null-vs-empty-schrodinger-s-range&quot;&gt;Understanding NULL vs empty: Schrödinger&#x27;s Range&lt;a class=&quot;zola-anchor&quot; href=&quot;#understanding-null-vs-empty-schrodinger-s-range&quot; aria-label=&quot;Anchor link for: understanding-null-vs-empty-schrodinger-s-range&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Ranges can be NULL or empty, and these are completely different things. NULL is Schrödinger&#x27;s range - you haven&#x27;t looked in the box yet, so it could be anything or nothing. Empty is when you&#x27;ve opened the box and confirmed it&#x27;s empty.&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s see this in practice:&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;-- NULL range: we don&amp;#39;t know what the period is&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; seat_holds_native (seat_id, user_session_id, hold_period)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;42&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;abc-123&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- empty range: we know the period is explicitly &amp;quot;nothing&amp;quot;&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; seat_holds_native (seat_id, user_session_id, hold_period)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;43&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;def-456&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;empty&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;The main difference between them is when it comes to handling.&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 NULL&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange &amp;amp;&amp;amp; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[2025-11-01, 2025-11-08)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Result: NULL (not true, not false—we don&amp;#39;t know)&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;SELECT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;empty&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange &amp;amp;&amp;amp; &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[2025-11-01, 2025-11-08)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Result: false (we know it doesn&amp;#39;t overlap)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And you can check for them in your queries using built-in function &lt;code&gt;isempty&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: #6A737D;&quot;&gt;-- check for NULL (like any column)&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; seat_holds_native &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; hold_period &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IS 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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- check for empty (special function)&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; seat_holds_native &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; isempty(hold_period);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In practice, you&#x27;ll mostly use &lt;code&gt;NOT NULL&lt;&#x2F;code&gt; constraints to prevent NULL ranges entirely. Empty ranges are useful but rare - usually for representing cancelled&#x2F;void periods you need to keep for special purposes - like audit trail.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;practical-integer-ranges-for-tiered-pricing&quot;&gt;Practical Integer ranges for Tiered pricing&lt;a class=&quot;zola-anchor&quot; href=&quot;#practical-integer-ranges-for-tiered-pricing&quot; aria-label=&quot;Anchor link for: practical-integer-ranges-for-tiered-pricing&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;While we introduced ranges we mostly paid attention to the date&#x2F;time handling the usefulness of range types goes well beyond that. One of the practical applications where integer ranges provide real values can be demostrated on tiered pricing.&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; quantity_discounts&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;    discount_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BIGINT 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;    product_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;    quantity_range INT4RANGE &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&gt;    discount_percentage &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;5&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    -- no overlapping tiers&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    EXCLUDE &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;USING&lt;&#x2F;span&gt;&lt;span&gt; GIST (&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        product_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WITH =&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;        quantity_range &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WITH&lt;&#x2F;span&gt;&lt;span&gt; &amp;amp;&amp;amp;&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;&#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; quantity_discounts (product_id, quantity_range, discount_percentage) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    -- 1-9 units: no discount&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: #9ECBFF;&quot;&gt;&amp;#39;[1,10)&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;00&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: #6A737D;&quot;&gt;    -- 10-49 units: 5% off&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: #9ECBFF;&quot;&gt;&amp;#39;[10,50)&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: #79B8FF;&quot;&gt;00&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: #6A737D;&quot;&gt;    -- 50-99 units: 10% off&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: #9ECBFF;&quot;&gt;&amp;#39;[50,100)&amp;#39;&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;00&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: #6A737D;&quot;&gt;    -- 100+ units: 15% off&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: #9ECBFF;&quot;&gt;&amp;#39;[100,1000)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;15&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;00&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;-- verify what discount we offer for ordering 75 units?&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; discount_percentage&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; quantity_discounts&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; product_id &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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  AND&lt;&#x2F;span&gt;&lt;span&gt; quantity_range @&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; 75&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: #6A737D;&quot;&gt;-- Result: 10.00&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;making-bad-data-impossible&quot;&gt;Making Bad Data Impossible&lt;a class=&quot;zola-anchor&quot; href=&quot;#making-bad-data-impossible&quot; aria-label=&quot;Anchor link for: making-bad-data-impossible&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;If the introduction of the range types provided the case for cleaner schema you can go ahead and make hard limits structurally impossible. While this is not advocating for the transition of the full business logic into database schema, you can eliminate the edge cases that should never make it to the 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 TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; promotional_campaigns&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;    campaign_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BIGINT 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 style=&quot;color: #F97583;&quot;&gt;    name TEXT 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;    active_period TSTZRANGE &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&gt;    budget_range NUMRANGE &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&gt;    discount_percentage &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;5&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    -- campaigns must be at least 1 days long&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    CONSTRAINT&lt;&#x2F;span&gt;&lt;span&gt; campaigns_minimum_duration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        CHECK&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;upper&lt;&#x2F;span&gt;&lt;span&gt;(active_period) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; lower&lt;&#x2F;span&gt;&lt;span&gt;(active_period) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;=&lt;&#x2F;span&gt;&lt;span&gt; INTERVAL &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;1 days&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    -- budget must be between $1000 and $100000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    CONSTRAINT&lt;&#x2F;span&gt;&lt;span&gt; campaigns_valid_budget&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        CHECK&lt;&#x2F;span&gt;&lt;span&gt; (budget_range &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span&gt;@ numrange(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1000&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;100000&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[]&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    -- active period must not be empty&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    CONSTRAINT&lt;&#x2F;span&gt;&lt;span&gt; campaigns_valid_period&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        CHECK&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;NOT&lt;&#x2F;span&gt;&lt;span&gt; isempty(active_period))&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;While this example demonstrates hypothetical example within the schema definition, please remember they shouldn&#x27;t be used to implement business process. The goal of the constraints is to enforce data integrity, i.e. structure requirements (minimum duration, non-empty data), physical or domain boundaries. Any other logic should make it&#x27;s way either to application logic or parts that are easier to modify (think functions).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;multiranges-when-one-range-is-not-enough&quot;&gt;Multiranges: When one range is not enough&lt;a class=&quot;zola-anchor&quot; href=&quot;#multiranges-when-one-range-is-not-enough&quot; aria-label=&quot;Anchor link for: multiranges-when-one-range-is-not-enough&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Up until now, we&#x27;ve been working with single continuous ranges. But what happens when you need to represent fragmented ranges? In past you needed a separate table with a foreign key relationship. With multiranges, you can store multiple non-contiguous ranges in a single column.&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL 14 introduced multirange types for all the built-in range types:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int4multirange&lt;&#x2F;code&gt;, &lt;code&gt;int8multirange&lt;&#x2F;code&gt;, &lt;code&gt;nummultirange&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;tsmultirange&lt;&#x2F;code&gt;, &lt;code&gt;tstzmultirange&lt;&#x2F;code&gt;, &lt;code&gt;datemultirange&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The real power of multiranges lies in &lt;strong&gt;schema density&lt;&#x2F;strong&gt; and &lt;strong&gt;query efficiency&lt;&#x2F;strong&gt;. We can prove this by comparing the cost of storing and querying the exact same data using two different valid range schemas.&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s consider storing 20 fragmented and non-contiguous periods - a pattern common for historical 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;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; user_periods_single_range&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;    period_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BIGINT 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;    user_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;    active_period TSTZRANGE &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;NOT NULL&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;CREATE INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; user_periods_single_range_gist_idx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    ON&lt;&#x2F;span&gt;&lt;span&gt; user_periods_single_range&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    USING&lt;&#x2F;span&gt;&lt;span&gt; gist (active_period);&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;-- 20 fragmented periods for user_id 42 (20 rows total)&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; user_periods_single_range (user_id, active_period)&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 style=&quot;color: #79B8FF;&quot;&gt;    42&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;    tstzrange(&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;2025-01-01&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz +&lt;&#x2F;span&gt;&lt;span&gt; (i &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2&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; days&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)::interval,&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;2025-01-01&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz +&lt;&#x2F;span&gt;&lt;span&gt; (i &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2&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 style=&quot;color: #F97583;&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39; days&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)::interval&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;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;20&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; s(i);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;compared to 1 row for the same 20 periods aggregated using &lt;code&gt;range_agg&lt;&#x2F;code&gt; function to consolidate 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;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; user_periods_multirange&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;    user_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INTEGER 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;    active_periods TSTZMULTIRANGE &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;NOT NULL&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;CREATE INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; user_periods_multirange_gist_idx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    ON&lt;&#x2F;span&gt;&lt;span&gt; user_periods_multirange&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    USING&lt;&#x2F;span&gt;&lt;span&gt; gist (active_periods);&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;-- consolidate the 20 TSTZRANGE rows into 1 TSTZMULTIRANGE row&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; user_periods_multirange (user_id, active_periods)&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;    user_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    -- range_agg function automatically handles merging adjacent ranges&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    range_agg(active_period)::TSTZMULTIRANGE&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; user_periods_single_range&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; user_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 42&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GROUP BY&lt;&#x2F;span&gt;&lt;span&gt; user_id;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;consider now the difference between&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;&#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; period_id, user_id&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; user_periods_single_range&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    user_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 42&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    AND&lt;&#x2F;span&gt;&lt;span&gt; active_period @&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;2025-01-20 12:00:00+00&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz&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 user_periods_single_range  (cost=4.19..13.67 rows=1 width=12)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Recheck Cond: (active_period @&amp;gt; &amp;#39;2025-01-20 13:00:00+01&amp;#39;::timestamp with time zone)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Filter: (user_id = 42)&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 user_periods_single_range_gist_idx  (cost=0.00..4.19 rows=6 width=0)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         Index Cond: (active_period @&amp;gt; &amp;#39;2025-01-20 13:00:00+01&amp;#39;::timestamp with time zone)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and same version of the consolidated 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&gt;EXPLAIN ANALYZE&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; user_id, active_periods&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; user_periods_multirange&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    user_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 42&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    AND&lt;&#x2F;span&gt;&lt;span&gt; active_periods @&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;2025-01-20 12:00:00+00&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz&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 user_periods_multirange_pkey on user_periods_multirange  (cost=0.15..8.17 rows=1 width=36)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Index Cond: (user_id = 42)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Filter: (active_periods @&amp;gt; &amp;#39;2025-01-20 13:00:00+01&amp;#39;::timestamp with time zone)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Giving you significant reduction in the query cost. And while the example was simple enough for the demonstration purposes you can easily define the helper schema for better indexable data access and opportunities to reduce the storage requirements.&lt;&#x2F;p&gt;
&lt;p&gt;One important note is that subtle change with the &lt;code&gt;&amp;amp;&amp;amp;&lt;&#x2F;code&gt; operators. Whereas with single range &lt;code&gt;&amp;amp;&amp;amp;&lt;&#x2F;code&gt; operator checks if two continues ranges overlap, for multiranges the operator checks if ANY range in multirange overlaps.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;creating-custom-range-types&quot;&gt;Creating custom range types&lt;a class=&quot;zola-anchor&quot; href=&quot;#creating-custom-range-types&quot; aria-label=&quot;Anchor link for: creating-custom-range-types&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;While PostgreSQL provides built-in range types for common data types, you can create custom range types for any data type that has a meaningful ordering. Let&#x27;s demostrate this with a type for IP address ranges.&lt;&#x2F;p&gt;
&lt;p&gt;To create a custom range type, you need to provide a subtype difference function that tells PostgreSQL how to calculate the &quot;distance&quot; between two values:&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;-- function to calculate difference between two IP addresses using bigint&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 OR REPLACE FUNCTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; inet_diff&lt;&#x2F;span&gt;&lt;span&gt;(x &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INET&lt;&#x2F;span&gt;&lt;span&gt;, y &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INET&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;RETURNS&lt;&#x2F;span&gt;&lt;span&gt; FLOAT8 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    SELECT&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;        (host(x)::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;inet -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;0.0.0.0&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;inet&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        (host(y)::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;inet -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;0.0.0.0&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;inet&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;    )::float8;&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;LANGUAGE SQL&lt;&#x2F;span&gt;&lt;span&gt; IMMUTABLE STRICT;&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;-- custom inetrange type&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 TYPE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; inetrange&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS RANGE&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;    subtype &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;= inet&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;    subtype_diff &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; inet_diff&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;-- convert CIDR ranges to inetranges&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 OR REPLACE FUNCTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; cidr_to_inetrange&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;cidr CIDR&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;RETURNS&lt;&#x2F;span&gt;&lt;span&gt; inetrange &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    SELECT&lt;&#x2F;span&gt;&lt;span&gt; inetrange(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        host(network($&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;inet&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;        host(broadcast($&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;inet&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;[]&amp;#39;&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;LANGUAGE SQL&lt;&#x2F;span&gt;&lt;span&gt; IMMUTABLE STRICT;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now you can use these custom types just like the built-in ones:&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; ip_blocklists&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;    blocklist_name &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT 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;    blocked_range inetrange &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;NOT NULL&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; ip_blocklists (blocklist_name, blocked_range) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&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: #9ECBFF;&quot;&gt;&amp;#39;attack #434401&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, cidr_to_inetrange(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;192.168.1.0&#x2F;24&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;	(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;attack #434401 (1)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[203.0.113.50,203.0.113.99]&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;    (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;attack #434401 (2)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[203.0.113.143,203.0.113.159]&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;and locate which attack has been assigned to particular malicious IP address.&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; blocklist_name, blocked_range&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; ip_blocklists&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; blocked_range @&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;192.168.1.25&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INET&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; blocklist_name |        blocked_range&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; attack #434401 | [192.168.1.0,192.168.1.255]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;But wait a second, wasn&#x27;t the fragmented nature of the ranges used in case for multiranges? Building real-life production and auto adaptive block list would most likely soon create extremely fragmented set of targets to block.&lt;&#x2F;p&gt;
&lt;p&gt;Can we defined it for our custom range types? Well no, because PostgreSQL is amazing! Since PostgreSQL 14 every time you define custom range type, it will automatically create corresponding multirange! Making it easy to consolidate the fragmented data corresponding to individual attack to multiranges.&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; typname &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; pg_type &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; typname &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LIKE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;inet%range&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;    typname&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; inetmultirange&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; inetrange&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Danger zone! When creating a custom range type the &lt;strong&gt;subtype_diff&lt;&#x2F;strong&gt; function is more than just simple helper. It plays &lt;strong&gt;important role in indexing and query performance&lt;&#x2F;strong&gt;. It really tells PostgreSQL planner how far apart the values in range are, which is crucial when building GiST indexes for ranges.&lt;&#x2F;p&gt;
&lt;p&gt;In our example above, if &lt;code&gt;inet_diff&lt;&#x2F;code&gt; returned &lt;code&gt;0&lt;&#x2F;code&gt; for every pair of IP addresses, PostgreSQL would think all ranges are &quot;equally close&quot;. This would lead to un-balanced indexes, with large hotposts in the indexes. End result would be that range operators would effectively be almost as slow as sequantial scans.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;performance-deep-dive-gist-vs-gin&quot;&gt;Performance deep dive: GiST vs GIN&lt;a class=&quot;zola-anchor&quot; href=&quot;#performance-deep-dive-gist-vs-gin&quot; aria-label=&quot;Anchor link for: performance-deep-dive-gist-vs-gin&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Throughout this article, we&#x27;ve been using GiST indexes almost exclusively, particularly when enforcing exclusion constraints. But PostgreSQL also supports GIN indexes for range types, and understanding when to use each can make the difference between a query that completes in milliseconds versus one that grinds your database to a halt.&lt;&#x2F;p&gt;
&lt;p&gt;Before we deep dive, let&#x27;s recap what those two indexes do. &lt;strong&gt;GiST (Generalized Search Tree)&lt;&#x2F;strong&gt; is a balanced tree structure that organizes ranges by their bounding boxes, and grouping those that are &quot;close together&quot; in same tree nodes. While &lt;strong&gt;GIN (Generalized Inverted Index)&lt;&#x2F;strong&gt; would decompose ranges into their components and indexing those. Therefore GiST works for continous ranges (timestamps and numerical values), while GIN works for discrete ranges (as you can&#x27;t generate unpredictable range of values of floats for example). Given this characteristics you can almost certainly say GIN indexes are almost always going to be bigger compared to the GiST ones, as they are always trying to index a continous space.&lt;&#x2F;p&gt;
&lt;p&gt;The most important thing to know upfront? As we already used without explicitely mentioning it - &lt;strong&gt;you can&#x27;t use GIN with&lt;&#x2F;strong&gt; EXCLUDE &lt;strong&gt;constraints.&lt;&#x2F;strong&gt; GiST is mandatory there.&lt;&#x2F;p&gt;
&lt;p&gt;Therefore while GiST index is going to be preferred for cases like&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; seat_holds&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;    hold_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BIGINT 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;    seat_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;    hold_period TSTZRANGE &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;NOT NULL&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;the GIN index is actually preferred for&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; venue_blackouts&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;    venue_id &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&gt;    blocked_dates DATERANGE &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;NOT NULL&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;The reason is simple - dates are discrete type. There&#x27;s only 365 (or 366) combinations in a given year. While timestamps have microsecond precision - millions of possible values per day.&lt;&#x2F;p&gt;
&lt;p&gt;The complexity of the index types is far beyond this article scope, so let&#x27;s just iterate over basic rules what index type to use. Most applications should just use GiST and move on. The performance difference rarely matters until you&#x27;re dealing with millions of rows and very specific query patterns. Don&#x27;t prematurely optimize - GiST is the safe, versatile default that works well for almost everything. You can always add a GIN index later if profiling shows it would help. PostgreSQL&#x27;s query planner is smart enough to pick the better index when both are available.&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;From my experience range types represent one of PostgreSQL&#x27;s most underutilized features, yet they offer immediate benefits: cleaner schemas, built-in data integrity, and query patterns that feel natural once you embrace them. What started as a solution to prevent double-booking seats evolved into a comprehensive look at how treating ranges as first-class citizens transforms your database design.&lt;&#x2F;p&gt;
&lt;p&gt;But we&#x27;ve really just scratched the surface. Timestamp ranges in particular open an entire world of possibilities we haven&#x27;t touched - &lt;strong&gt;temporal tables&lt;&#x2F;strong&gt;. The ability to maintain complete historical records with automatic versioning, query data &quot;as of&quot; any point in time, and track changes without cluttering your schema with audit columns deserves its own deep dive. That&#x27;s a topic for a future article.&lt;&#x2F;p&gt;
&lt;p&gt;For now, the next time you reach for separate &lt;code&gt;start&lt;&#x2F;code&gt; and &lt;code&gt;end&lt;&#x2F;code&gt; columns, stop and ask yourself: &quot;Should this be a range?&quot; More often than not, the answer is yes. Your future self - the one debugging a problems in least convenient time - will thank you.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>PostgreSQL maintenance without superuser</title>
        <published>2025-09-13T20:55:00+00:00</published>
        <updated>2025-09-13T20:55:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/postgresql-predefined-roles/"/>
        <id>https://boringsql.com/posts/postgresql-predefined-roles/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/postgresql-predefined-roles/">&lt;p&gt;How many people&#x2F;services have superuser access to your PostgreSQL cluster(s)?
Did you ever ask why your software engineers might need it? Or your BI team?
Why those use cases require same privileges as someone who can drop your
databases?&lt;&#x2F;p&gt;
&lt;p&gt;The answer isn&#x27;t because these operations are inherently dangerous - it&#x27;s
because PostgreSQL historically offered limited options for operational access
or simply because not enough people are aware of the options. So the common practice
is to either got basic permissions or handover the keys to the kingdom.&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL&#x27;s &lt;strong&gt;built-in predefined roles&lt;&#x2F;strong&gt; solve this problem by providing
purpose-built privileges for common maintenance tasks. Instead of granting
superuser access for routine operations, you can delegate specific capabilities - monitoring teams get comprehensive observability access, backup services get data reading capabilities, and maintenance scripts get precisely the permissions
they need, nothing more.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-are-predefined-roles&quot;&gt;What are Predefined Roles?&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-are-predefined-roles&quot; aria-label=&quot;Anchor link for: what-are-predefined-roles&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;PostgreSQL&#x27;s built-in administrative roles are purpose-built permission sets
that solve the superuser dilemma for common maintenance tasks. Out of the box,
there are 15 predefined roles that provide granular access to specific
operational capabilities without requiring full superuser privileges.&lt;&#x2F;p&gt;
&lt;p&gt;While you can view their list and description in &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;predefined-roles.html&quot;&gt;official
documentation&lt;&#x2F;a&gt;,
in this article we will explore them bit more thoroughly and at the same time
look into system catalogs to understand them better. The individual roles can be
grouped by their functionality and most of them are quite easy to grasp, ranging
from simple monitoring access to powerful filesystem operations that require
careful consideration.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Data Access Role&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pg_database_owner&lt;&#x2F;code&gt; - Database-specific ownership (special case)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pg_read_all_data&lt;&#x2F;code&gt; - Read access to all tables, views, sequences&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pg_write_all_data&lt;&#x2F;code&gt; - Write access to all tables, views, sequences&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Monitoring &amp;amp; Observability&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pg_monitor&lt;&#x2F;code&gt; - Which is actually monitoring meta role that contains 3 roles listed below&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pg_read_all_settings&lt;&#x2F;code&gt; - Configuration access&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pg_read_all_stats&lt;&#x2F;code&gt; - Statistics views&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pg_stat_scan_tables&lt;&#x2F;code&gt; - Table scanning for stats&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;System Operations&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pg_signal_backend&lt;&#x2F;code&gt; - Cancel queries, terminate sessions&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pg_checkpoint&lt;&#x2F;code&gt; - Run CHECKPOINT command&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pg_maintain&lt;&#x2F;code&gt; - VACUUM, ANALYZE, REINDEX operations (PostgreSQL 17+)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pg_signal_autovacuum_worker&lt;&#x2F;code&gt; - Signal autovacuum worker (PostgreSQL 18+)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;File System Access&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pg_read_server_files&lt;&#x2F;code&gt; - Read files from server filesystem&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pg_write_server_files&lt;&#x2F;code&gt; - Write files to server filesystem&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pg_execute_server_program&lt;&#x2F;code&gt; - Execute programs on server&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;And &lt;strong&gt;specialised use cases&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pg_create_subscription&lt;&#x2F;code&gt; - Logical replication management&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pg_use_reserved_connections&lt;&#x2F;code&gt; - Connection reservation (PostgreSQL 16+)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;why-use-predefined-roles&quot;&gt;Why Use Predefined Roles?&lt;a class=&quot;zola-anchor&quot; href=&quot;#why-use-predefined-roles&quot; aria-label=&quot;Anchor link for: why-use-predefined-roles&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The primary benefit of predefined roles is &lt;strong&gt;expanding the pool of users who can
safely manage PostgreSQL databases&lt;&#x2F;strong&gt;. Traditional PostgreSQL administration
created an artificial binary choice: either users had basic access with limited
capabilities, or they required full superuser privileges for operational tasks.
This forced many organizations to grant excessive permissions simply to perform
routine operations like monitoring, backups, or maintenance.&lt;&#x2F;p&gt;
&lt;p&gt;Predefined roles break this limitation by allowing more granular control over
operational tasks without distributing superuser privileges. Instead of having a
small number of highly privileged superusers handling all administrative work,
organizations can now delegate specific capabilities to appropriate operational
roles - monitoring teams get monitoring access, backup services get data reading
capabilities, and maintenance scripts get precisely the permissions they need.&lt;&#x2F;p&gt;
&lt;p&gt;The deeper benefit is &lt;strong&gt;abstraction&lt;&#x2F;strong&gt;. Not only do you switch from manually
managing permissions on individual system objects, you switch to managing
logical capability sets. This creates a clear separation between &lt;em&gt;what&lt;&#x2F;em&gt; you want
to allow and &lt;em&gt;how&lt;&#x2F;em&gt; it&#x27;s actually implemented.&lt;&#x2F;p&gt;
&lt;p&gt;The advantage is clearly demonstrated in following 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;-- granting select on each schema separately&lt;&#x2F;span&gt;&lt;&#x2F;span&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; USAGE &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; finance, hr, app, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;audit TO&lt;&#x2F;span&gt;&lt;span&gt; analytics_team;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GRANT SELECT ON&lt;&#x2F;span&gt;&lt;span&gt; ALL TABLES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; finance &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; analytics_team;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GRANT SELECT ON&lt;&#x2F;span&gt;&lt;span&gt; ALL SEQUENCES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; finance &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; analytics_team;&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;-- versus one time setup&lt;&#x2F;span&gt;&lt;&#x2F;span&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_read_all_data &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; analytics_team;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The predefined roles approach automatically covers:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;All current tables, views and sequences&lt;&#x2F;li&gt;
&lt;li&gt;All schemas&lt;&#x2F;li&gt;
&lt;li&gt;Future objects created after the &lt;code&gt;GRANT&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;And the good news is the benefits don&#x27;t end here. The real thing on operational level comes with the scope of those predefined roles - they &lt;strong&gt;apply at cluster level&lt;&#x2F;strong&gt; (instead on database level). Only notable exception is role &lt;code&gt;pg_database_owner&lt;&#x2F;code&gt; which we will cover next.&lt;&#x2F;p&gt;
&lt;p&gt;Before going there, let&#x27;s briefly mention second benefit, and that&#x27;s the fact any new operational features in PostgreSQL will be covered by those predefined roles automatically.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-evolution-of-predefined-roles&quot;&gt;The Evolution of Predefined Roles&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-evolution-of-predefined-roles&quot; aria-label=&quot;Anchor link for: the-evolution-of-predefined-roles&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Understanding when and why predefined roles were introduced provides important
insight into PostgreSQL&#x27;s operational priorities and helps explain some of the
design decisions. The journey from a single role in PostgreSQL 9.6 to today&#x27;s
comprehensive set reflects real-world pain points that PostgreSQL developers
encountered in production environments.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL 9.6 (2016)&lt;&#x2F;strong&gt; introduced first predefined role &lt;code&gt;pg_signal_backend&lt;&#x2F;code&gt; to
address common frustration - inability to cancel running query without giving
away superuser rights.&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;-- This became possible in 9.6 without superuser privileges&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_cancel_backend(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;12345&lt;&#x2F;span&gt;&lt;span&gt;);  &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Cancel a query&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_terminate_backend(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;12345&lt;&#x2F;span&gt;&lt;span&gt;);  &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Terminate a session&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL 10 (2017)&lt;&#x2F;strong&gt; brought biggest expansion of predefined roles with,
adding four monitoring specific roles to improve database observability, and
support growing importance of database monitoring in production environments.&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;-- Create the monitoring user&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 USER&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; postgres_exporter&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; WITH PASSWORD&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;monitoring_password&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Grant comprehensive monitoring access&lt;&#x2F;span&gt;&lt;&#x2F;span&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_monitor &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; postgres_exporter;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Giving access to variety of metrics and settings. With single &lt;code&gt;GRANT&lt;&#x2F;code&gt; you
can access data:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;From pg_read_all_settings (inherited via pg_monitor) to &lt;code&gt;SELECT * FROM pg_settings&lt;&#x2F;code&gt; - Configuration parameters for alerting on misconfigurations
and configuration metrics like shared_buffers, work_mem, max_connections&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;From pg_read_all_stats (inherited via pg_monitor):&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;-- Database-wide statistics (connections, transactions, blocks read&#x2F;written)&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; pg_stat_database&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Current query activity and connection states&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; pg_stat_activity&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Replication lag and status&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; pg_stat_replication&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Background writer performance&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; pg_stat_bgwriter&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Lock contention monitoring&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; pg_locks&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;From pg_stat_scan_tables (inherited via pg_monitor):&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Enhanced table statistics collection with sequential scan information&lt;&#x2F;li&gt;
&lt;li&gt;More detailed I&#x2F;O statistics for table access patterns&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The &lt;code&gt;pg_monitor&lt;&#x2F;code&gt; role is particularly clever in its design. Rather than being a
single monolithic permission set, it&#x27;s actually a composite of the other three
roles. This allows for granular access when needed - you can grant
pg_read_all_stats for basic monitoring without including configuration access,
or grant the full pg_monitor bundle for comprehensive monitoring capabilities.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL 11 (2018)&lt;&#x2F;strong&gt; laid the foundation for secure file system operations
with the introduction of three powerful roles that fundamentally changed how
PostgreSQL handles server-side file access:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pg_read_server_files&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pg_write_server_files&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pg_execute_server_program&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Addressing many long-standing issues with providing file system access, like ETL
processes that needs to read CSV files and data processing pipelines often involving
external programs.&lt;&#x2F;p&gt;
&lt;p&gt;The file system roles represented a significant security advancement. Before
PostgreSQL 11, operations like server-side COPY FROM file were superuser-only,
forcing administrators to either grant excessive privileges or find workarounds.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL 14 (2021)&lt;&#x2F;strong&gt; introduced three significant roles that addressed different
data access patterns:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pg_read_all_data&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pg_write_all_data&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pg_database_owner&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The first two solved a common backup and analytics challenge. The special case is
&lt;code&gt;pg_database_owner&lt;&#x2F;code&gt; which we will cover later.&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;-- create ETL extraction user&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 USER&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; etl_extractor&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; WITH PASSWORD&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;extract_password&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: #F97583;&quot;&gt;GRANT&lt;&#x2F;span&gt;&lt;span&gt; pg_read_all_data &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; etl_extractor;&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;-- create ETL loader with write access&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 USER&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; etl_loader&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; WITH PASSWORD&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;loader_password&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: #F97583;&quot;&gt;GRANT&lt;&#x2F;span&gt;&lt;span&gt; pg_write_all_data &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; etl_loader;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;One big caveat that comes with those ALL data roles is that they still (despite what the name might suggest) respect Row Level Security policies (RLS). This is design choice of PostgreSQL which prioritises security by default - even for users with broad data access. The only way to avoid RLS is to explicitly grant &lt;code&gt;BYPASSRLS&lt;&#x2F;code&gt; role attribute.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL 15 (2022)&lt;&#x2F;strong&gt; expanded set with &lt;code&gt;pg_checkpoint&lt;&#x2F;code&gt;, reflecting the
reality that checkpoint operations are sometimes needed for maintenance but
don&#x27;t require full superuser privileges.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL 16 (2023)&lt;&#x2F;strong&gt; expanded the list by introduction of&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pg_use_reserved_connections&lt;&#x2F;code&gt; - resolving a critical problem with high-traffic
databases, where previously only superusers could access reserved connections.
Introduction of this roles expanded the pool of potential users who might be
able to resolve incidents or high-load situations. For example with &lt;code&gt;pg_signal_backend&lt;&#x2F;code&gt; grant.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pg_create_subscription&lt;&#x2F;code&gt; - with ability to create &lt;a href=&quot;&#x2F;guides&#x2F;mastering-logical-replication&#x2F;&quot;&gt;Logical Replication&lt;&#x2F;a&gt; subscriptions and confirming the place of PostgreSQL in the distributed database scenarios.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL 17 (2024)&lt;&#x2F;strong&gt; finally saw &lt;code&gt;pg_maintain&lt;&#x2F;code&gt;, role that had a turbulent
development history (initially committed for PostgreSQL 16), allowing
non-superusers to perform crucial maintenance tasks like VACUUM, ANALYZE, REINDEX,
REFRESH MATERIALIZED VIEW, CLUSTER, and LOCK TABLE.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;code&gt;pg_maintain&lt;&#x2F;code&gt; role is particularly valuable for:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Automated maintenance scripts that can now run with minimal privileges, reducing the security
footprint of scheduled maintenance operations.&lt;&#x2F;li&gt;
&lt;li&gt;Operational teams managing multiple databases who need consistent maintenance capabilities without
requiring superuser access to each database.&lt;&#x2F;li&gt;
&lt;li&gt;Cloud and managed environments where maintenance operations need to be delegated to operations staff without
granting broader system access.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL 18 (2025)&lt;&#x2F;strong&gt; continues the trend with &lt;code&gt;pg_signal_autovacuum_worker&lt;&#x2F;code&gt;
expanding the ability for non-superusers to signal autovacuum workers
specifically, enabling them to cancel vacuum operations on specific tables or
terminate autovacuum sessions that may be causing performance issues during
critical periods.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-magic-of-pg-database-owner&quot;&gt;The Magic of pg_database_owner&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-magic-of-pg-database-owner&quot; aria-label=&quot;Anchor link for: the-magic-of-pg-database-owner&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;If you paid attention, there&#x27;s one role that stands apart from all others -
&lt;code&gt;pg_database_owner&lt;&#x2F;code&gt;. While it does not provide any specific privileges that
wouldn&#x27;t be possible without superuser access, it serves as a marker for the
owner of the database. This role is crucial for maintaining database ownership
and ensuring that the database is managed by the correct user.&lt;&#x2F;p&gt;
&lt;p&gt;Before PostgreSQL 15 you would have &lt;code&gt;public&lt;&#x2F;code&gt; schema owned by &lt;code&gt;postgres&lt;&#x2F;code&gt; user.&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;demo=&amp;gt; \dn&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  List of schemas&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Name  |  Owner&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; public | postgres&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It came with the baggage of relaxed default permission, allowing every user
(PUBLIC) to create objects. Making this a maintenance nightmare.&lt;&#x2F;p&gt;
&lt;p&gt;Starting with PostgreSQL 15 and above, the &lt;code&gt;public&lt;&#x2F;code&gt; schema is owned by &lt;code&gt;pg_database_owner&lt;&#x2F;code&gt; role.&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;demo=# \dn&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      List of schemas&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Name  |       Owner&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; public | pg_database_owner&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Unlike any other roles, &lt;code&gt;pg_database_owner&lt;&#x2F;code&gt; has a unique behaviour - it
membership changes with the current database. As the code snippet above shows
the &quot;shape-shifting nature&quot; of this role fixed the &lt;code&gt;public&lt;&#x2F;code&gt; schema and
complexity of the permissions associated with it.&lt;&#x2F;p&gt;
&lt;p&gt;Another special behaviour is that the role itself does not come with any
permissions. Let that sink in - zero permissions. Its power only comes from what
you grant it or inherit. It opens up system for elaborate &lt;em&gt;template
inheritance&lt;&#x2F;em&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: #6A737D;&quot;&gt;-- define functionality in template1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;\c template1 postgres&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 OR REPLACE FUNCTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; db_owner_stats&lt;&#x2F;span&gt;&lt;span&gt;()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; RETURN&lt;&#x2F;span&gt;&lt;span&gt; ...  &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SECURITY&lt;&#x2F;span&gt;&lt;span&gt; DEFINER;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GRANT EXECUTE ON FUNCTION&lt;&#x2F;span&gt;&lt;span&gt; db_owner_stats()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; TO&lt;&#x2F;span&gt;&lt;span&gt; pg_database_owner;&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;-- and let it automatically apply to all new databases&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 DATABASE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; app_prod&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; OWNER&lt;&#x2F;span&gt;&lt;span&gt; first_app;&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 DATABASE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; demo_prod&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; OWNER&lt;&#x2F;span&gt;&lt;span&gt; second_app;&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;-- both first_app and second_app automatically inherit db_owner_stats function on their databases&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And when we are talking about magic, the special status of the role prevents
this role to be granted to any other role.&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;demo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;#&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; GRANT&lt;&#x2F;span&gt;&lt;span&gt; pg_database_owner &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; labs_app;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ERROR:  &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;role&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;pg_database_owner&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt; cannot have &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;explicit&lt;&#x2F;span&gt;&lt;span&gt; members&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&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;Predefined roles transform PostgreSQL administration from ad-hoc permission
management into systematic capability delegation. They solve the fundamental
problem of needing operational access without operational trust.&lt;&#x2F;p&gt;
&lt;p&gt;The evolution continues - each PostgreSQL release adds new predefined roles
addressing real-world operational challenges. The pattern is established:
identify common pain points, create focused roles, eliminate the need for
superuser privileges in routine operations.&lt;&#x2F;p&gt;
&lt;p&gt;The next time you reach for superuser access to solve an operational problem,
check if PostgreSQL has already provided a predefined role for exactly that
purpose. You&#x27;ll likely find it already has one.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Beyond the Basics of Logical Replication</title>
        <published>2025-06-24T07:10:00+00:00</published>
        <updated>2025-06-24T07:10:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/logical-replication-beyond-the-basics/"/>
        <id>https://boringsql.com/posts/logical-replication-beyond-the-basics/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/logical-replication-beyond-the-basics/">&lt;p&gt;With &lt;a href=&quot;&#x2F;posts&#x2F;logication-replication-introduction&#x2F;&quot;&gt;First Steps with Logical Replication&lt;&#x2F;a&gt; we set up a basic working replication between a publisher and a subscriber and were introduced to the fundamental concepts. In this article, we&#x27;re going to expand on the practical aspects of logical replication operational management, monitoring, and dive deep into the foundations of logical decoding.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;initial-data-copy&quot;&gt;Initial Data Copy&lt;a class=&quot;zola-anchor&quot; href=&quot;#initial-data-copy&quot; aria-label=&quot;Anchor link for: initial-data-copy&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;As we demonstrated in the first part, when setting up the subscriber, you can choose (or not to) to rely on initial data copy using the option &lt;code&gt;WITH (copy_data = false)&lt;&#x2F;code&gt;. While the default copy is incredibly useful behavior, this default has characteristics you should understand before using it in a production environment.&lt;&#x2F;p&gt;
&lt;p&gt;The mechanism effectively asks the publisher to copy the table data by taking a snapshot (courtesy of MVCC), sending it to the subscriber, and thanks to the replication slot &quot;bookmark,&quot; seamlessly continues streaming the changes from the point the snapshot was taken.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Simplicity&lt;&#x2F;strong&gt; is the key feature here, as a single command handles the snapshot, transfer, and transition to ongoing streaming.&lt;&#x2F;p&gt;
&lt;p&gt;The trade-off you&#x27;re making is when it comes to &lt;strong&gt;performance&lt;&#x2F;strong&gt;, solely due to the fact that it&#x27;s using a single process per table. Behind the scenes the initial synchronization is done using &lt;code&gt;COPY&lt;&#x2F;code&gt; given the best possible performance. While it works almost instantly for majority of the tables, you will encounter notable delay and overhead when dealing with tables with gigabytes of data.&lt;&#x2F;p&gt;
&lt;p&gt;Although parallelism can be controlled by the &lt;code&gt;max_sync_workers_per_subscription&lt;&#x2F;code&gt; configuration parameter, it still might leave you waiting for hours (and days) for any real-life database to get replicated. You can monitor whether the tables have already been synchronized or are still waiting&#x2F;in progress using the &lt;code&gt;pg_subscription_rel&lt;&#x2F;code&gt; catalog.&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; srrelid::regclass &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; table_name, srsubstate&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_subscription_rel;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Where each table will have one of the following states:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;i&lt;&#x2F;code&gt; not yet started&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;d&lt;&#x2F;code&gt; copy is in progress&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;s&lt;&#x2F;code&gt; syncing (or waiting for confirmation)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;r&lt;&#x2F;code&gt; done &amp;amp; replicating&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Luckily, the state &lt;code&gt;r&lt;&#x2F;code&gt; indicates that the streaming of changes can start even if not all tables are synchronized. Nevertheless, the &lt;strong&gt;replication slot retains the LSN position&lt;&#x2F;strong&gt;, which means that the publisher will &lt;strong&gt;retain WAL files&lt;&#x2F;strong&gt; until the subscriber has caught up.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;manual-synchronization-for-production-workloads&quot;&gt;Manual Synchronization for Production Workloads&lt;a class=&quot;zola-anchor&quot; href=&quot;#manual-synchronization-for-production-workloads&quot; aria-label=&quot;Anchor link for: manual-synchronization-for-production-workloads&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;As mentioned above, the implicit copy might bring performance trade-offs that are simply too much for production use, unless you consider it while designing your logical replication topology. In all other scenarios, going with manual synchronization is the way forward.&lt;&#x2F;p&gt;
&lt;p&gt;The entire process has one leading principle: the data you load manually must be consistent with the point in time from which the logical replication stream begins. This is achieved by &lt;strong&gt;creating a logical replication slot before (or synchronized with) the point at which you restore the data (PITR)&lt;&#x2F;strong&gt; and enabling it once the data is transferred. Only this will allow you to correctly apply all subsequent changes on the subscriber.&lt;&#x2F;p&gt;
&lt;p&gt;There are several ways to achieve this, and you will have to evaluate the mechanism based on the available constraints in your particular use case:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Use the backup &amp;amp; restore mechanism that supports Point-in-Time Recovery (PITR) if the amount of data to be synchronized approaches the total size of the backed-up data (i.e., most of the tables).&lt;&#x2F;li&gt;
&lt;li&gt;Orchestrate the data change ingestion on the publisher with the backup, by stopping the incoming changes (for example, during a planned maintenance window) and only restoring them when a consistent snapshot is available to a known LSN. This is for cases where you can control table ingestion and the amount of data being transferred fits the maintenance window.&lt;&#x2F;li&gt;
&lt;li&gt;If none of this is available, you might be able to manually advance replication slots to a predefined LSN. Please understand, this should be a last resort as it might be considered expert domain.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;The recommended way for the most reliable backup and restore handling is the use of &lt;strong&gt;pgBackRest&lt;&#x2F;strong&gt;, as it allows you to restore a particular backup in advance and apply only changes needed later to advance to the selected Point-in-Time, hence significantly reducing the time required for the initial sync. On the other hand, if you are only using a small portion of the data within the backup on your subscriber, it might create resource constraints not acceptable for the setup.&lt;&#x2F;p&gt;
&lt;p&gt;While &lt;strong&gt;pg_dump can&#x27;t be considered a reliable backup tool&lt;&#x2F;strong&gt;, it might help you in case you are talking about making a consistent backup of a small subset of tables using the &quot;synchronized snapshot&quot; created using &lt;code&gt;pg_export_snapshot&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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;pg_dump -d publisher_db --snapshot=&amp;quot;000004A2-1&amp;quot; --data-only -Fc -f initial_data.dump&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Please note, the topic of consistent backups and Point-in-Time-Restore is way beyond this logical replication guide.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;monitoring-logical-replication&quot;&gt;Monitoring Logical Replication&lt;a class=&quot;zola-anchor&quot; href=&quot;#monitoring-logical-replication&quot; aria-label=&quot;Anchor link for: monitoring-logical-replication&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Once you set up your publishers and subscribers and have the initial data in place, it&#x27;s time to start thinking about how to keep that process running.&lt;&#x2F;p&gt;
&lt;p&gt;For day-to-day operations, the basic building block for monitoring is the &lt;code&gt;pg_replication_slots&lt;&#x2F;code&gt; catalog. Sample query:&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    slot_name,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    plugin,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    slot_type,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    active,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pg_size_pretty(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)&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;AS&lt;&#x2F;span&gt;&lt;span&gt; wal_retained_size&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_replication_slots&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    slot_type &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;logical&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;giving you 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;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;slot_name         | my_sample_subscription&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;plugin            | pgoutput&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;slot_type         | logical&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;active            | t&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;wal_retained_size | 49 MB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You should look for two things:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;active&lt;&#x2F;code&gt; indicating whether the slot is being used by any connection. While this might be acceptable for shorter durations, it might be a sign that the subscriber is down or not configured properly.&lt;&#x2F;li&gt;
&lt;li&gt;The computed &lt;code&gt;wal_retained_size&lt;&#x2F;code&gt; is a critical metric. An increasing value indicates problems consuming changes by the subscriber, and your cluster might be at risk of running out of disk space.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Both values are important as they might indicate problems you need to pay attention to. The &lt;code&gt;active&lt;&#x2F;code&gt; flag might indicate dangling slots, where the subscriber has been destroyed (or simply wiped) without properly dropping the subscription—hence leaving the replication slot behind. In such cases, the only direct response is dropping the slot by its name:&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_drop_replication_slot(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;my_dangling_slot&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;If you want to dig deeper, you can also use &lt;code&gt;pg_stat_replication&lt;&#x2F;code&gt; that provides more (albeit volatile) data about the replication status. It can give you other metrics, like &lt;code&gt;replay_lag&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;To wrap up the monitoring part, you can either design your custom checks or use a modified version of the query above:&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  slot_name,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  CASE WHEN&lt;&#x2F;span&gt;&lt;span&gt; active&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    THEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    ELSE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  END as&lt;&#x2F;span&gt;&lt;span&gt; active_status,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;as&lt;&#x2F;span&gt;&lt;span&gt; wal_bytes_retained,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  extract(epoch &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from now&lt;&#x2F;span&gt;&lt;span&gt;()) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;as time&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_replication_slots&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; slot_type &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;logical&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;as part of a Grafana alerting rule:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;Alert Rule&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Inactive Replication Slot&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;Condition&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: #85E89D;&quot;&gt; Query&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; active_status&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: #85E89D;&quot;&gt; Reducer&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; last()&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: #85E89D;&quot;&gt; Evaluator&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; below 1&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: #85E89D;&quot;&gt; For&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 30m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;   # if inactive for 30+ minutes&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: #85E89D;&quot;&gt;Additional conditions (AND)&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: #85E89D;&quot;&gt; Query&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; wal_bytes_retained&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: #85E89D;&quot;&gt; Reducer&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; last()&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: #85E89D;&quot;&gt; Evaluator&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; above 1073741824&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;   # 1GB limit&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;Nevertheless, you need to evaluate the monitoring details applicable for your particular use case(s). There might also be other options, for example, if your architecture allows for it, to use &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;postgresqlco.nf&#x2F;doc&#x2F;en&#x2F;param&#x2F;max_slot_wal_keep_size&#x2F;&quot;&gt;max_slot_wal_keep_size&lt;&#x2F;a&gt; and mark the replication slot invalid and release the WAL files when a particular slot falls behind the configured value (for example, 100GB). While this might sound reasonable, you must considered whatever your application or use case is able to recover from such a data loss.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;evolving-publications&quot;&gt;Evolving Publications&lt;a class=&quot;zola-anchor&quot; href=&quot;#evolving-publications&quot; aria-label=&quot;Anchor link for: evolving-publications&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Going back to the first part of our journey into logical replication, there&#x27;s an important distinction to make. While we have described the initial use case, in real-life your publisher schema will change as time goes by. Tables will be created and  included implicitly (via &lt;code&gt;FOR ALL TABLES&lt;&#x2F;code&gt;) or explicitly added&#x2F;removed.&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&lt;&#x2F;span&gt;&lt;span&gt; PUBLICATION my_publication &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ADD TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;new_table&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;ALTER&lt;&#x2F;span&gt;&lt;span&gt; PUBLICATION my_publication&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; DROP TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.old_table;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;But what happens when you add a new table? For the existing subscription, the answer is not much at all. The only way for the subscriber to reflect the changes in the underlying configuration is to &lt;strong&gt;refresh the subscription&lt;&#x2F;strong&gt;. It&#x27;s a process where PostgreSQL compares the current subscription table list with the publication table list, (if configured to do so) starts the initial synchronization process and once finished goes to apply streamed changes.&lt;&#x2F;p&gt;
&lt;p&gt;As mentioned, you can refresh a subscription with or without copying the initial data. The &lt;code&gt;copy_data&lt;&#x2F;code&gt; option controls this behavior and is currently the only supported setting.&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&lt;&#x2F;span&gt;&lt;span&gt; SUBSCRIPTION my_subscription&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    REFRESH PUBLICATION;&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;ALTER&lt;&#x2F;span&gt;&lt;span&gt; SUBSCRIPTION my_subscription&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    REFRESH PUBLICATION &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WITH&lt;&#x2F;span&gt;&lt;span&gt; (copy_data &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;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;When you remove a table from the publisher, the situation is much simpler as the changes for that particular table are no longer considered for logical decoding.&lt;&#x2F;p&gt;
&lt;p&gt;But what happens if you were to add a new table to the publication without refreshing the subscription? Despite the WAL files including changes for all tables, and the logical decoding process will process &lt;strong&gt;all published table changes&lt;&#x2F;strong&gt;, there will be no additional WAL retention to consider. The subscriber will advance &lt;code&gt;confirmed_flush_lsn&lt;&#x2F;code&gt; as before, simply because the initial state for a newly added table has not yet been recorded and streamed changes will be (for that moment) ignored.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;logical-decoding&quot;&gt;Logical Decoding&lt;a class=&quot;zola-anchor&quot; href=&quot;#logical-decoding&quot; aria-label=&quot;Anchor link for: logical-decoding&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;If you paid attention throughout our journey or already worked a bit with logical replication, you might have come across something called the &lt;code&gt;pgoutput&lt;&#x2F;code&gt; plugin. This is the built-in default mechanism responsible for logical decoding—a mechanism that goes through the WAL stream and transforms it into a higher-level format.&lt;&#x2F;p&gt;
&lt;p&gt;An example is, instead of physical replication—byte X at offset Y, in page Z—the logical decoding translates the WAL stream to row-level changes. It uses the context awareness of the source database to understand the changes and change from object identifier to the schema, table, and column name, as well as the respective values (both old and new). It also provides a way to assemble the changes into the correct transaction flow.&lt;&#x2F;p&gt;
&lt;p&gt;The plugin&#x27;s job is to format that output in a particular way and by itself is not aware of:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;any transport layer details&lt;&#x2F;li&gt;
&lt;li&gt;replication slots&lt;&#x2F;li&gt;
&lt;li&gt;any retry logic&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;PostgreSQL offers a robust framework for logical decoding. The built-in plugins are just a start, and developers can create custom output plugins. But not only that—we can also look inside the logical decoded stream manually.&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s consider this 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;-- create new publication&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&lt;&#x2F;span&gt;&lt;span&gt; PUBLICATION all_data &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FOR&lt;&#x2F;span&gt;&lt;span&gt; ALL TABLES;&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;-- create new replication slot with pgoutput plugin&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_create_logical_replication_slot(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;plugin_demo&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;pgoutput&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- sample table&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 TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; IF&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; NOT EXISTS&lt;&#x2F;span&gt;&lt;span&gt; demo_table (&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;INT 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 style=&quot;color: #F97583;&quot;&gt;    name 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&gt;    email &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 some changes with insert&#x2F;update&#x2F;delete&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; demo_table (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span&gt;, email) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;John Doe&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;john@example.com&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: #F97583;&quot;&gt;UPDATE&lt;&#x2F;span&gt;&lt;span&gt; demo_table &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; email &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;doe@example.com&amp;#39;&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;=&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DELETE FROM&lt;&#x2F;span&gt;&lt;span&gt; demo_table &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;=&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;&#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;-- get binary changes&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; pg_logical_slot_get_binary_changes(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;plugin_demo&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;NULL&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;NULL&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;proto_version&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;1&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;publication_names&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;all_data&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- drop the replication slot and the publication&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_drop_replication_slot(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;plugin_demo&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: #F97583;&quot;&gt;DROP TABLE&lt;&#x2F;span&gt;&lt;span&gt; demo_table;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DROP&lt;&#x2F;span&gt;&lt;span&gt; PUBLICATION all_data;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Giving us a peek into the recently performed changes (output is shortened):&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;    lsn       | xid  |                                                                 data&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; 1&#x2F;D6997380 | 1038 | \x4200000001d69974180&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 1&#x2F;D6997380 | 1038 | \x52000042057075626c696300646...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 1&#x2F;D6997380 | 1038 | \x49000042054e000374000...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 1&#x2F;D6997448 | 1038 | \x430000000001d6997...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 1&#x2F;D6997448 | 1039 | \x4200000001d6997...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 1&#x2F;D6997448 | 1039 | \x49000042054e0....&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 1&#x2F;D6997510 | 1039 | \x430000000001d6...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Which is correct, but won&#x27;t help us to demonstrate the flow. Don&#x27;t forget &lt;code&gt;pgoutput&lt;&#x2F;code&gt; is the actual plugin used for logical decoding, and is binary. But don&#x27;t despair, PostgreSQL offers the &lt;code&gt;test_decoding&lt;&#x2F;code&gt; plugin which decodes the changes into a human-readable text representation.&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;-- setup the publication&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&lt;&#x2F;span&gt;&lt;span&gt; PUBLICATION all_data &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FOR&lt;&#x2F;span&gt;&lt;span&gt; ALL TABLES;&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;-- create replication slot with test_decoding plugin&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_create_logical_replication_slot(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;plugin_demo&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;test_decoding&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;&#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; IF&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; NOT EXISTS&lt;&#x2F;span&gt;&lt;span&gt; demo_table (&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;INT 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 style=&quot;color: #F97583;&quot;&gt;    name 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&gt;    email &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 some changes with insert&#x2F;update&#x2F;delete&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; demo_table (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span&gt;, email) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;John Doe&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;john@example.com&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: #F97583;&quot;&gt;UPDATE&lt;&#x2F;span&gt;&lt;span&gt; demo_table &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; email &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;doe@example.com&amp;#39;&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;=&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DELETE FROM&lt;&#x2F;span&gt;&lt;span&gt; demo_table &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;=&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;&#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;-- get human readable changes&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; pg_logical_slot_get_changes(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;plugin_demo&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;NULL&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- drop the replication slot and the publication&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_drop_replication_slot(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;plugin_demo&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: #F97583;&quot;&gt;DROP TABLE&lt;&#x2F;span&gt;&lt;span&gt; demo_table;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DROP&lt;&#x2F;span&gt;&lt;span&gt; PUBLICATION all_data;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This time the output is actually going to be helpful.&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;    lsn       | xid  |                                                        data&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; 1&#x2F;D69CCC30 | 1048 | BEGIN 1048&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 1&#x2F;D69CCC98 | 1048 | table public.demo_table: INSERT: id[integer]:1 name[text]:&amp;#39;John Doe&amp;#39; email[text]:&amp;#39;john@example.com&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 1&#x2F;D69CCDC0 | 1048 | COMMIT 1048&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 1&#x2F;D69CCDC0 | 1049 | BEGIN 1049&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 1&#x2F;D69CCDC0 | 1049 | table public.demo_table: UPDATE: id[integer]:1 name[text]:&amp;#39;John Doe&amp;#39; email[text]:&amp;#39;doe@example.com&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 1&#x2F;D69CCE50 | 1049 | COMMIT 1049&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 1&#x2F;D69CCE50 | 1050 | BEGIN 1050&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 1&#x2F;D69CCE50 | 1050 | table public.demo_table: DELETE: id[integer]:1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 1&#x2F;D69CCEC0 | 1050 | COMMIT 1050&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Just reading it, you can easily follow the sequence of the transaction (auto-commit in psql) and track individual changes.&lt;&#x2F;p&gt;
&lt;p&gt;We can also reiterate the importance of &lt;code&gt;REPLICA IDENTITY&lt;&#x2F;code&gt; with this example. If you double-check the DDL for &lt;code&gt;demo_table&lt;&#x2F;code&gt;, you will see &lt;code&gt;IDENTITY&lt;&#x2F;code&gt; used, which by its definition will create a primary key. With default replica identity, you are effectively relying on that as the primary source of identifying data. You can use the &lt;code&gt;test_decoding&lt;&#x2F;code&gt; plugin to demonstrate the verbosity it will create.&lt;&#x2F;p&gt;
&lt;p&gt;(In this example, leaving the setup and tear down.)&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 TABLE&lt;&#x2F;span&gt;&lt;span&gt; demo_table &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;REPLICA IDENTITY&lt;&#x2F;span&gt;&lt;span&gt; FULL;&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; demo_table (id, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span&gt;, email) OVERRIDING &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SYSTEM VALUE VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;999&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;John Doe&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;john@example.com&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: #F97583;&quot;&gt;UPDATE&lt;&#x2F;span&gt;&lt;span&gt; demo_table &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; email &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;doe@example.com&amp;#39;&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;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 999&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;DELETE FROM&lt;&#x2F;span&gt;&lt;span&gt; demo_table &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;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 999&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;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; pg_logical_slot_get_changes(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;plugin_demo&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;NULL&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;NULL&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 you can observe the changes. While by its nature &lt;code&gt;INSERT&lt;&#x2F;code&gt; gives you the same output (all values are being sent), &lt;code&gt;UPDATE&lt;&#x2F;code&gt; and &lt;code&gt;DELETE&lt;&#x2F;code&gt; are no longer relying on the primary key. Instead, they have to provide all values, for the &lt;code&gt;UPDATE&lt;&#x2F;code&gt; case both old and new 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;lsn  | 1&#x2F;D69F8770&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;xid  | 1062&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;data | table public.demo_table: UPDATE: old-key: id[integer]:999 name[text]:&amp;#39;John Doe&amp;#39; email[text]:&amp;#39;john@example.com&amp;#39; new-tuple: id[integer]:999 name[text]:&amp;#39;John Doe&amp;#39; email[text]:&amp;#39;doe@exa&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;mple.com&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and for &lt;code&gt;DELETE&lt;&#x2F;code&gt; the old 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;lsn  | 1&#x2F;D69F8828&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;xid  | 1063&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;data | table public.demo_table: DELETE: id[integer]:999 name[text]:&amp;#39;John Doe&amp;#39; email[text]:&amp;#39;doe@example.com&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Making it obvious how misconfigured replica identity might increase the amount of data decoded and replicated.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fine-grained-publication-control&quot;&gt;Fine-grained Publication Control&lt;a class=&quot;zola-anchor&quot; href=&quot;#fine-grained-publication-control&quot; aria-label=&quot;Anchor link for: fine-grained-publication-control&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Before we introduce the advanced replication topologies in later parts of this series, let&#x27;s have a look at how you can precisely control what data to replicate to the subscribers. Fine-grained control helps you to control replication overhead, reduce network traffic, enhance security, and ensure the subscribers only receive the data they need.&lt;&#x2F;p&gt;
&lt;p&gt;The first option to filter the data is by explicit &lt;code&gt;column_list&lt;&#x2F;code&gt;, allowing you to exclude sensitive (PII or similar) or unnecessary data. When selecting the columns, you should include the primary key or the columns behind replica identity; if not, PostgreSQL will add them automatically.&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; PUBLICATION hr_analytics&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    FOR TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; hr&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;employees&lt;&#x2F;span&gt;&lt;span&gt; (employee_id, first_name, last_name, department, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;start_date&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;Similar to column selection, you can control what operations a publication replicates. The available options are &#x27;insert&#x27;, &#x27;update&#x27;, &#x27;delete&#x27;, and &#x27;truncate&#x27;.&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; PUBLICATION hr_analytics&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    FOR TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; hr&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;employees&lt;&#x2F;span&gt;&lt;span&gt; (employee_id, first_name, last_name, department, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;start_date&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;        WITH&lt;&#x2F;span&gt;&lt;span&gt; (publish &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;insert,delete&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;securing-logical-replication&quot;&gt;Securing Logical Replication&lt;a class=&quot;zola-anchor&quot; href=&quot;#securing-logical-replication&quot; aria-label=&quot;Anchor link for: securing-logical-replication&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;In the previous section, we hit a crucial element of logical replication—limiting access to the publication. Otherwise, what purpose would it have to limit fields published if another publication would still offer them without restrictions? So far in this series, we have relied for simplicity on &lt;code&gt;SUPERUSER&lt;&#x2F;code&gt; access to create and manage publications. To design logical replication for real-life production use, you need a least-privilege model.&lt;&#x2F;p&gt;
&lt;p&gt;Luckily for us, the PostgreSQL security model for logical replication follows the same rules as for regular queries. &lt;strong&gt;If a user can&#x27;t SELECT from a specific table, or specific columns or rows, they can&#x27;t include them in a publication either.&lt;&#x2F;strong&gt; Therefore, this section assumes you are already familiar with the PostgreSQL permission model.&lt;&#x2F;p&gt;
&lt;p&gt;The high-level overview of the permissions needed is that the replication user must have &lt;code&gt;CONNECT&lt;&#x2F;code&gt; permission on the database, &lt;code&gt;USAGE&lt;&#x2F;code&gt; on the schema, and &lt;code&gt;SELECT&lt;&#x2F;code&gt; on specific tables (columns). You also need to ensure (if applicable) row-level security (RLS) is correctly managed.&lt;&#x2F;p&gt;
&lt;p&gt;Using the fine-grained example used above, we can demonstrate the setup. First, creating a role with &lt;code&gt;LOGIN&lt;&#x2F;code&gt; and &lt;code&gt;REPLICATION&lt;&#x2F;code&gt; clauses and granting database and schema access:&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 ROLE&lt;&#x2F;span&gt;&lt;span&gt; hr_analytics_role &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WITH LOGIN&lt;&#x2F;span&gt;&lt;span&gt; REPLICATION &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;PASSWORD&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;a_strong_password&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GRANT CONNECT ON DATABASE&lt;&#x2F;span&gt;&lt;span&gt; my_publisher_db &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; hr_analytics_role;&lt;&#x2F;span&gt;&lt;&#x2F;span&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; USAGE &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; hr &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; hr_analytics_role;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The crucial step is to correctly define the &lt;code&gt;GRANT&lt;&#x2F;code&gt; for selecting the 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;GRANT SELECT&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    (employee_id, first_name, last_name, department, hire_date)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; hr&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;employees&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; TO&lt;&#x2F;span&gt;&lt;span&gt; hr_analytics_role;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This approach ensures that even if someone gains access to the replication role, they cannot expose data beyond what was explicitly granted. The publication will respect these column-level restrictions, creating a secure boundary for your replicated data.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wrap-up&quot;&gt;Wrap Up&lt;a class=&quot;zola-anchor&quot; href=&quot;#wrap-up&quot; aria-label=&quot;Anchor link for: wrap-up&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Today we&#x27;ve moved beyond the basics of logical replication to cover the essential practices for production environments and expanded the understanding of how it actually works.&lt;&#x2F;p&gt;
&lt;p&gt;This article is second part of the upcoming guide &lt;a href=&quot;&#x2F;guides&#x2F;mastering-logical-replication&#x2F;&quot;&gt;Mastering Logical Replication
in PostgreSQL&lt;&#x2F;a&gt;. If you are interested in
the topic, please consider subscribing to get the latest articles as they are
published.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>First steps with Logical Replication in PostgreSQL</title>
        <published>2025-06-11T00:00:00+00:00</published>
        <updated>2025-06-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/logication-replication-introduction/"/>
        <id>https://boringsql.com/posts/logication-replication-introduction/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/logication-replication-introduction/">&lt;p&gt;Most applications start with a single PostgreSQL database, but over time, the need to scale out, distribute the load, or integrate naturally arises. PostgreSQL&#x27;s logical replication is one of the features that meets these demands by &lt;strong&gt;streaming row-level changes&lt;&#x2F;strong&gt; from one PostgreSQL instance to another, all using a &lt;strong&gt;publish-subscribe&lt;&#x2F;strong&gt; model. Logical replication is more than an advanced feature; it provides a flexible framework you can build on to further distribute and integrate PostgreSQL within your architecture.&lt;&#x2F;p&gt;
&lt;p&gt;In this article, we will start with the foundation, explore the core ideas behind logical replication, and learn how to use it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;physical-vs-logical-replication&quot;&gt;Physical vs. Logical Replication&lt;a class=&quot;zola-anchor&quot; href=&quot;#physical-vs-logical-replication&quot; aria-label=&quot;Anchor link for: physical-vs-logical-replication&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Before we can dive deeper, let&#x27;s understand the role of replication in PostgreSQL and how it&#x27;s built on top of the &lt;strong&gt;Write-Ahead Log (WAL)&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The WAL is a sequential, append-only log that records every change made to the cluster data. For durability purposes, all modifications are first written to the WAL and only then permanently written to disk. This allows PostgreSQL to recover from crashes by replaying logged changes.&lt;&#x2F;p&gt;
&lt;p&gt;Versioned changes, necessitated by concurrent transactions, are managed through &lt;strong&gt;Multi-Version Concurrency Control (MVCC)&lt;&#x2F;strong&gt;. Instead of overwriting data directly, MVCC creates multiple versions of rows, allowing each transaction to see a consistent snapshot of the database. It is the WAL that captures these versioned changes along with the transactional metadata to ensure data consistency at any given point in time.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Physical replication&lt;&#x2F;strong&gt; is built directly on the Write-Ahead Log. It enables streaming of the binary WAL data from the primary server to one or more standby servers (replicas), effectively creating a byte-for-byte copy of the entire cluster. This requirement makes the replicas read-only, making them ideal candidates for failover or scaling purposes.&lt;&#x2F;p&gt;
&lt;p&gt;Compared to this, &lt;strong&gt;Logical replication&lt;&#x2F;strong&gt;, while also being built on top of the WAL data, takes a fundamentally different approach. Instead of streaming raw change data, logical replication &lt;strong&gt;decodes the WAL into logical, row-level changes&lt;&#x2F;strong&gt; – such as &lt;code&gt;INSERT&lt;&#x2F;code&gt;, &lt;code&gt;UPDATE&lt;&#x2F;code&gt;, and &lt;code&gt;DELETE&lt;&#x2F;code&gt; – and only then sends them to the subscribers using a Publish-Subscribe model. Compared to physical replication, this allows selective replication, while allowing writable subscribers which are not strictly tied to a single publisher. This might increase the flexibility of available setups, however logical replication &lt;strong&gt;does not replicate DDL changes&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;&#x2F;th&gt;&lt;th&gt;Physical&lt;&#x2F;th&gt;&lt;th&gt;Logical&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Data Streamed&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Binary WAL segments&lt;&#x2F;td&gt;&lt;td&gt;Row-level SQL changes&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Scope&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Byte-for-byte stream&lt;&#x2F;td&gt;&lt;td&gt;Selected tables&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Node Type&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Read-only standby&lt;&#x2F;td&gt;&lt;td&gt;Fully writable instance&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;PostgreSQL Version&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;All servers must match major version&lt;&#x2F;td&gt;&lt;td&gt;Supports across versions&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Database Schema&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Changes automatically replicated&lt;&#x2F;td&gt;&lt;td&gt;Changes must be applied on subscriber(s) separately&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Use Case&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;Failover, high-availability, and read scaling&lt;&#x2F;td&gt;&lt;td&gt;Integration, zero-downtime upgrades and schema migrations, complex topologies&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Physical replication is your go-to for &lt;strong&gt;high availability&lt;&#x2F;strong&gt; and &lt;strong&gt;disaster recovery&lt;&#x2F;strong&gt;, where you want a fast, exact copy of the entire database cluster that can take over in case of failure. It’s simple to set up and very efficient but limited in flexibility.&lt;&#x2F;p&gt;
&lt;p&gt;Logical replication shines when you need &lt;strong&gt;fine-grained control&lt;&#x2F;strong&gt; over what data is replicated, require &lt;strong&gt;writable replicas&lt;&#x2F;strong&gt;, or want to &lt;strong&gt;integrate&lt;&#x2F;strong&gt; PostgreSQL with other systems or versions. It’s ideal for &lt;strong&gt;zero-downtime upgrades&lt;&#x2F;strong&gt;, &lt;strong&gt;multi-region deployments&lt;&#x2F;strong&gt;, and building &lt;strong&gt;scalable, modular architectures&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;setting-up-logical-replication&quot;&gt;Setting up Logical Replication&lt;a class=&quot;zola-anchor&quot; href=&quot;#setting-up-logical-replication&quot; aria-label=&quot;Anchor link for: setting-up-logical-replication&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Enough of boring theory for now. To get started, you will need two instances of PostgreSQL – it&#x27;s up to you whether you provision two virtual machines or two clusters running on the same computer. We will call one &lt;strong&gt;publisher&lt;&#x2F;strong&gt; and the other &lt;strong&gt;subscriber&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;preparing-the-publisher&quot;&gt;Preparing the Publisher&lt;a class=&quot;zola-anchor&quot; href=&quot;#preparing-the-publisher&quot; aria-label=&quot;Anchor link for: preparing-the-publisher&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;First, you need to prepare the publisher to emit the logical changes. You will need to modify &lt;code&gt;postgresql.conf&lt;&#x2F;code&gt; (or add particular &lt;code&gt;conf.d&lt;&#x2F;code&gt; configuration) with the following parameters:&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;wal_level = logical&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;max_replication_slots = 10&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;max_wal_senders = 10&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Your publisher also needs to be reachable by the subscriber, in most cases via a TCP socket.&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;listen_addresses = &amp;#39;*&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Here, the configuration is:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;postgresqlco.nf&#x2F;doc&#x2F;en&#x2F;param&#x2F;wal_level&#x2F;&quot;&gt;&lt;code&gt;wal_level&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; set to &lt;code&gt;logical&lt;&#x2F;code&gt; is a crucial piece of the configuration. It tells PostgreSQL how much information to write to the WAL, in this case, to support logical decoding.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;postgresqlco.nf&#x2F;doc&#x2F;en&#x2F;param&#x2F;max_replication_slots&#x2F;&quot;&gt;&lt;code&gt;max_replication_slots&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; defines the maximum number of replication slots that can be created on the server. Each logical replication subscription needs its own slot.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;postgresqlco.nf&#x2F;doc&#x2F;en&#x2F;param&#x2F;max_wal_senders&#x2F;&quot;&gt;&lt;code&gt;max_wal_senders&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; should be set high enough to accommodate all expected concurrent replication connections from subscribers. Each active replication subscription consumes a wal_sender slot.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;should match the maximum number of connections from the publisher (primary) to subscribers or replication clients. The number should be higher than or equal to the number of replication slots to avoid replication problems.&lt;&#x2F;p&gt;
&lt;p&gt;You will also need to configure client authentication in &lt;code&gt;pg_hba.conf&lt;&#x2F;code&gt; to allow the subscriber to connect for replication (for simplification, we allow all users):&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;# TYPE      DATABASE        USER            ADDRESS                 METHOD&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;host        replication     all             subscriber_ip&#x2F;32        scram-sha-256&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;While for the purposes of the article we will assume the use of a superuser, making it convenient:&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 USER&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; my_user_name&lt;&#x2F;span&gt;&lt;span&gt; SUPERUSER &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;PASSWORD&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;my_secure_password&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;It is recommended to use a dedicated replication user for real-life deployments:&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 USER&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; replication_user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; WITH&lt;&#x2F;span&gt;&lt;span&gt; REPLICATION &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ENCRYPTED PASSWORD&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;my_secure_password&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;Once configured, restart PostgreSQL for the configuration changes to take effect. After that, connect to the server and prepare the environment.&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;-- example for psql&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 DATABASE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; logical_demo_publisher&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 logical_demo_publisher;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Create a sample table and seed initial 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;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; products&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;BIGINT 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 style=&quot;color: #F97583;&quot;&gt;    name TEXT 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;    category &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&gt;    price &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DECIMAL&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&gt;    stock_quantity &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT DEFAULT&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;&#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;TIMESTAMP WITH TIME ZONE DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; CURRENT_TIMESTAMP,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    updated_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TIMESTAMP WITH TIME ZONE DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; CURRENT_TIMESTAMP,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    description 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&gt;    is_active &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BOOLEAN DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; TRUE&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;-- seed some data (10 records)&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; products (&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    name&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;    category,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    price,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    stock_quantity,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    description&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;    is_active&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;SELECT&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;Product Batch &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; s&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS name&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;    CASE&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;s&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        WHEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; THEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Electronics&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        WHEN&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; THEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Books&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        WHEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; THEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Home Goods&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        WHEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 3&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; THEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Apparel&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        ELSE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Miscellaneous&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    END AS&lt;&#x2F;span&gt;&lt;span&gt; category,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    ROUND&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; 500&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&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: #F97583;&quot;&gt;numeric&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;AS&lt;&#x2F;span&gt;&lt;span&gt; price,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    FLOOR&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; 200&lt;&#x2F;span&gt;&lt;span&gt;)::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;int AS&lt;&#x2F;span&gt;&lt;span&gt; stock_quantity,&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;Auto-generated description for product ID &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; s&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&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;. Lorem ipsum dolor sit amet, consectetur adipiscing elit.&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS description&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;s&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&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 style=&quot;color: #F97583;&quot;&gt; &amp;lt;&amp;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;AS&lt;&#x2F;span&gt;&lt;span&gt; is_active&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;10&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; s(id);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The final step for the &lt;strong&gt;publisher&lt;&#x2F;strong&gt; is to create a publication to define what data we want to publish.&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; PUBLICATION my_publication &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FOR TABLE&lt;&#x2F;span&gt;&lt;span&gt; products;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;preparing-the-subscriber&quot;&gt;Preparing the Subscriber&lt;a class=&quot;zola-anchor&quot; href=&quot;#preparing-the-subscriber&quot; aria-label=&quot;Anchor link for: preparing-the-subscriber&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Next, we will use our &lt;strong&gt;subscriber&lt;&#x2F;strong&gt; instance to receive the logical changes. There&#x27;s no need to make any configuration changes &lt;em&gt;just&lt;&#x2F;em&gt; for subscribing, as it&#x27;s not emitting changes itself (please note the &quot;just&quot;).&lt;&#x2F;p&gt;
&lt;p&gt;And create the target database and schema. The schema creation is an important part as:&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;-- example for psql&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 DATABASE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; my_subscriber_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 my_subscriber_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; products&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;BIGINT 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 style=&quot;color: #F97583;&quot;&gt;    name TEXT 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;    category &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&gt;    price &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DECIMAL&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&gt;    stock_quantity &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT DEFAULT&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;&#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;TIMESTAMP WITH TIME ZONE DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; CURRENT_TIMESTAMP,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    updated_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TIMESTAMP WITH TIME ZONE DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; CURRENT_TIMESTAMP,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    description 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&gt;    is_active &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BOOLEAN DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; TRUE&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 we have the foundation for testing logical replication. The simplest way is to create a subscription using connection details and the name of the publication to subscribe to.&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; SUBSCRIPTION my_subscription&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    CONNECTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;host=publisher_ip_address port=5432 user=your_replication_user password=my_secure_password dbname=logical_demo_publisher&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    PUBLICATION my_publication;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;my_subscription&lt;&#x2F;code&gt; should be a descriptive name for your subscription.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;CONNECTION&lt;&#x2F;code&gt; defines how to connect to the publisher (which is a regular connection string and could also be &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.google.com&#x2F;search?q=&#x2F;posts&#x2F;postgresql-service-definition&#x2F;&quot;&gt;a service&lt;&#x2F;a&gt;).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;PUBLICATION&lt;&#x2F;code&gt; specifies the name of the publication you created earlier on the publisher.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;If your connection was correct, the subscription will by default start with the initial data sync and listen for incoming changes. If you have used the queries above, you can validate that the data is now on the subscriber.&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;#&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; SELECT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; count&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: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; products;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; count&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 style=&quot;color: #79B8FF;&quot;&gt;    10&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;The number of records should match the number of records created using &lt;code&gt;generate_series&lt;&#x2F;code&gt; above (10 in our example). You can go ahead and insert a single row again on your publisher instance and validate the data being replicated to the subscriber.&lt;&#x2F;p&gt;
&lt;p&gt;Congratulations! You have set up your first logical replication in PostgreSQL!&lt;&#x2F;p&gt;
&lt;h2 id=&quot;core-concepts-of-logical-replication&quot;&gt;Core Concepts of Logical Replication&lt;a class=&quot;zola-anchor&quot; href=&quot;#core-concepts-of-logical-replication&quot; aria-label=&quot;Anchor link for: core-concepts-of-logical-replication&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;That was easy, right? And that&#x27;s the goal. Now that you&#x27;ve seen logical replication in action, let&#x27;s delve deeper into the core concepts that made this work.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;publication&quot;&gt;Publication&lt;a class=&quot;zola-anchor&quot; href=&quot;#publication&quot; aria-label=&quot;Anchor link for: publication&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;As the name implies, &lt;strong&gt;a publication&lt;&#x2F;strong&gt; is where it all starts. It&#x27;s essentially a catalogue of data you offer from the publisher. You can &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;sql-createpublication.html&quot;&gt;publish&lt;&#x2F;a&gt; a number of objects:&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;--- specific tables&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&lt;&#x2F;span&gt;&lt;span&gt; PUBLICATION my_publication &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FOR TABLE&lt;&#x2F;span&gt;&lt;span&gt; products, orders;&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;--- everything in your database (make sure you really want to do this)&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&lt;&#x2F;span&gt;&lt;span&gt; PUBLICATION all_data &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FOR&lt;&#x2F;span&gt;&lt;span&gt; ALL TABLES;&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;--- replicate specific columns only (PostgreSQL 15 and higher)&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&lt;&#x2F;span&gt;&lt;span&gt; PUBLICATION generic_data &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FOR TABLE&lt;&#x2F;span&gt;&lt;span&gt; products (id, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span&gt;, price);&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;--- filter published data (PostgreSQL 15 and higher)&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&lt;&#x2F;span&gt;&lt;span&gt; PUBLICATION active_products &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FOR TABLE&lt;&#x2F;span&gt;&lt;span&gt; products &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; (is_active &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; true);&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;--- filter different operations&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&lt;&#x2F;span&gt;&lt;span&gt; PUBLICATION my_publication &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FOR TABLE&lt;&#x2F;span&gt;&lt;span&gt; products, orders&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WITH&lt;&#x2F;span&gt;&lt;span&gt; (publish &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;insert&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;--- or you can mix &amp;amp; match it to get exactly what you need&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&lt;&#x2F;span&gt;&lt;span&gt; PUBLICATION eu_customers &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FOR&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    TABLE&lt;&#x2F;span&gt;&lt;span&gt; customers (id, email, country, 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;        WHERE&lt;&#x2F;span&gt;&lt;span&gt; (country &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: #9ECBFF;&quot;&gt;&amp;#39;DE&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;FR&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;IT&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: #F97583;&quot;&gt;    TABLE&lt;&#x2F;span&gt;&lt;span&gt; orders (id, customer_id, total_amount)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        WHERE&lt;&#x2F;span&gt;&lt;span&gt; (total_amount &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; 100&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;WITH&lt;&#x2F;span&gt;&lt;span&gt; (publish &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;insert, update&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;As you can see, logical replication really gives you quite a lot of options and is far from &lt;strong&gt;the rigid, byte-for-byte copying&lt;&#x2F;strong&gt; of physical replication. This is exactly the characteristic that allows you to build complex topologies.&lt;&#x2F;p&gt;
&lt;p&gt;Once you set up your publication(s), you can review it:&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;--- in psql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;\dRp&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;\dRp+ my_publication&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&gt;--- using pg_catalog&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SELECT * FROM pg_publication_tables WHERE pubname = &amp;#39;my_publication&amp;#39;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;From these options, it&#x27;s easy to think of a publication as a customisable data feed.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;subscription&quot;&gt;Subscription&lt;a class=&quot;zola-anchor&quot; href=&quot;#subscription&quot; aria-label=&quot;Anchor link for: subscription&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;The other side of logical replication is &lt;strong&gt;a subscription&lt;&#x2F;strong&gt;. It defines what and how to consume events from a publisher. You subscribe to a publication using connection details and a number of options.&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;--- basic subscription with full connection detail&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&lt;&#x2F;span&gt;&lt;span&gt; SUBSCRIPTION my_subscription&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    CONNECTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;host=publisher_host port=5432 user=repl_user password=secret dbname=source_db&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    PUBLICATION my_publication;&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;-- subscription using service definition&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&lt;&#x2F;span&gt;&lt;span&gt; SUBSCRIPTION realtime_only&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    CONNECTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;service=my_source_db&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    PUBLICATION my_publication;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The default behaviour of the subscription is to &lt;strong&gt;copy existing data&lt;&#x2F;strong&gt;, &lt;strong&gt;start immediately&lt;&#x2F;strong&gt;, and continue with streaming data changes. Once you start experimenting with logical replication, you can control that behaviour.&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;--- subscribe without initial copy of the data&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&lt;&#x2F;span&gt;&lt;span&gt; SUBSCRIPTION streaming_only&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    CONNECTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;service=my_source_db&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    PUBLICATION my_publication&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    WITH&lt;&#x2F;span&gt;&lt;span&gt; (copy_data &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 defer the start for later&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&lt;&#x2F;span&gt;&lt;span&gt; SUBSCRIPTION manual_start&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    CONNECTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;service=my_source_db&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    PUBLICATION my_publication&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    WITH&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;enabled =&lt;&#x2F;span&gt;&lt;span&gt; false);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You can monitor your subscriptions:&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;--- in psql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;\dRs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;\dRs+ my_subscription&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&gt;--- or their status using pg_catalog&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SELECT subname, received_lsn, latest_end_lsn, latest_end_time&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;FROM pg_stat_subscription;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;replication-slot&quot;&gt;Replication Slot&lt;a class=&quot;zola-anchor&quot; href=&quot;#replication-slot&quot; aria-label=&quot;Anchor link for: replication-slot&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Publications and subscriptions establish the data flow, but there&#x27;s a critical piece missing: how does the publisher keep track of multiple subscribers reading the WAL at different speeds? &lt;strong&gt;Replication slots&lt;&#x2F;strong&gt; are the answer. They act as a persistent booking in the WAL stream that tracks exactly where each subscriber is (or was last time), &lt;strong&gt;ensuring no changes are lost&lt;&#x2F;strong&gt; even if the connection stops.&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s have a look at what a replication slot can tell us about itself.&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;#&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; pg_replication_slots;&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&gt;[ RECORD 1 ]&lt;&#x2F;span&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;slot_name           | my_subscription&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;plugin              | pgoutput&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;slot_type           | logical&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;datoid              | &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;16390&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;database&lt;&#x2F;span&gt;&lt;span&gt;            | my_db_source&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;temporary           | f&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;active              | t&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;active_pid          | &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;3162&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;xmin                |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;catalog_xmin        | &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;777&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;restart_lsn         | &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&#x2F;&lt;&#x2F;span&gt;&lt;span&gt;1DEFBF0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;confirmed_flush_lsn | &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&#x2F;&lt;&#x2F;span&gt;&lt;span&gt;1DEFC28&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;wal_status          | reserved&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;safe_wal_size       |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;two_phase           | f&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;inactive_since      |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;conflicting         | f&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;invalidation_reason |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;failover&lt;&#x2F;span&gt;&lt;span&gt;            | f&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;synced              | f&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The most interesting attributes are:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;slot_name&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;plugin&lt;&#x2F;code&gt; used for logical replication&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;slot_type&lt;&#x2F;code&gt; confirming it&#x27;s for logical replication&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;active&lt;&#x2F;code&gt; indicates whether there&#x27;s a subscriber reading from this slot&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;And most important of those being:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;restart_lsn&lt;&#x2F;code&gt; identifying the LSN where this slot &quot;holds&quot; WAL files from being released&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;confirmed_flush_lsn&lt;&#x2F;code&gt; being the last LSN position the subscriber confirmed it has successfully processed (i.e., applied).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;While we won&#x27;t go into details about WAL, it&#x27;s enough to say LSN (Log Sequence Number) is &lt;strong&gt;a unique address or position&lt;&#x2F;strong&gt; in the WAL that identifies the position in the stream of database changes.&lt;&#x2F;p&gt;
&lt;p&gt;In most cases, you &lt;strong&gt;don&#x27;t have to create replication slots manually&lt;&#x2F;strong&gt; – subscriptions create and manage them. The persistent nature of the slots guarantees they survive the restart of both publisher and subscriber.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;Please note&lt;&#x2F;em&gt;, if the subscription is not actively consuming the changes, the publisher PostgreSQL won&#x27;t release WAL files that contain changes a slot has not consumed. &lt;strong&gt;This prevents data loss, but can easily fill up disk if any of the subscribers fall too far behind.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;You can monitor the WAL retention per each replication slot.&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;#&lt;&#x2F;span&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;    slot_name,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    active,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; wal_retained&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_replication_slots;&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;-&lt;&#x2F;span&gt;&lt;span&gt;[ RECORD 1 ]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;+&lt;&#x2F;span&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;slot_name    | my_subscription&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;active       | t&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;wal_retained | &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;265&lt;&#x2F;span&gt;&lt;span&gt; MB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;replication-identity&quot;&gt;Replication Identity&lt;a class=&quot;zola-anchor&quot; href=&quot;#replication-identity&quot; aria-label=&quot;Anchor link for: replication-identity&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;As we have already covered, logical replication works with row-level changes, such as &lt;code&gt;INSERT&lt;&#x2F;code&gt;, &lt;code&gt;UPDATE&lt;&#x2F;code&gt;, and &lt;code&gt;DELETE&lt;&#x2F;code&gt;. And not all operations are equal. While &lt;code&gt;INSERT&lt;&#x2F;code&gt; is relatively straightforward (a new row is sent to the subscriber), for both &lt;code&gt;UPDATE&lt;&#x2F;code&gt; and &lt;code&gt;DELETE&lt;&#x2F;code&gt; operations, PostgreSQL needs to be able to uniquely identify the target row to modify on the subscriber.&lt;&#x2F;p&gt;
&lt;p&gt;This is managed by the &lt;code&gt;REPLICA IDENTITY&lt;&#x2F;code&gt; property of each published table. By default, if a table has a primary key (it has, right?), or you can specify it &lt;code&gt;USING INDEX&lt;&#x2F;code&gt; for a unique index, or as an alternative, specify &lt;code&gt;FULL&lt;&#x2F;code&gt; (should be generally avoided) to all old column&#x27;s row values to find the target row to update. Please note, PostgreSQL might default to &lt;code&gt;REPLICA IDENTITY FULL&lt;&#x2F;code&gt; if no primary key or suitable key index exists.&lt;&#x2F;p&gt;
&lt;p&gt;Whenever possible, please always:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Choose &lt;code&gt;REPLICA IDENTITY DEFAULT&lt;&#x2F;code&gt; (for primary key), giving you the most efficient choice. If you don&#x27;t have a primary key, please consider using it (a surrogate index is always an option).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;REPLICA IDENTITY USING INDEX index_name&lt;&#x2F;code&gt; if there would be a better (or smaller) unique index matching the business logic or architecture of your database model.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;There are special cases of what not to do when it comes to replication identity, but we will touch on those cases in the advanced section of this guide.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;schema-changes&quot;&gt;Schema Changes&lt;a class=&quot;zola-anchor&quot; href=&quot;#schema-changes&quot; aria-label=&quot;Anchor link for: schema-changes&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Although it was already mentioned, it&#x27;s vital to reiterate that logical replication, as it streams only row-level changes rather than full WAL segments, &lt;strong&gt;does not automatically replicate Data Definition Language (DDL) changes&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Any schema changes made on the publisher &lt;strong&gt;must be manually applied to all subscribers&lt;&#x2F;strong&gt; before data reflecting the change is replicated.&lt;&#x2F;p&gt;
&lt;p&gt;We will cover schema specifically in later parts of this guide, but to sum it up, this is the recommended order of operations when it comes to schema changes:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;(Optional) but recommended for breaking changes, pause replication where applicable.&lt;&#x2F;li&gt;
&lt;li&gt;Apply and verify DDL changes to the subscribers.&lt;&#x2F;li&gt;
&lt;li&gt;Apply and verify DDL changes on the publisher.&lt;&#x2F;li&gt;
&lt;li&gt;Resume the replication if applicable.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;While this might seem like a major drawback compared to physical replication, it&#x27;s the characteristic that gives us the most flexibility.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;other-considerations&quot;&gt;Other Considerations&lt;a class=&quot;zola-anchor&quot; href=&quot;#other-considerations&quot; aria-label=&quot;Anchor link for: other-considerations&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;As logical replication reliably handles row-level changes, it&#x27;s crucial to understand other specific limitations and behaviours. Specifically:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;It&#x27;s important to understand &lt;strong&gt;that sequences&lt;&#x2F;strong&gt; are not replicated. While they are used to generate the value on the publisher, and their value advances there, the corresponding sequence (if it exists) on the subscriber won&#x27;t be updated.&lt;&#x2F;li&gt;
&lt;li&gt;Other database objects won&#x27;t be replicated. While it might be understandable for views, stored procedures, triggers, and rules, you also can&#x27;t rely on it for materialized views.&lt;&#x2F;li&gt;
&lt;li&gt;Special consideration (similar to schema changes) must be paid to user-defined data types. If a replicated table uses a user-defined type (e.g., a column using an &lt;code&gt;ENUM&lt;&#x2F;code&gt;), the type must already be available on the subscriber(s) and have exactly the same name and structure. As &lt;code&gt;ENUM&lt;&#x2F;code&gt;s are represented as ordered sets, even the order of the values matters!&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;living-with-logical-replication&quot;&gt;Living with Logical Replication&lt;a class=&quot;zola-anchor&quot; href=&quot;#living-with-logical-replication&quot; aria-label=&quot;Anchor link for: living-with-logical-replication&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;In our previous example, we set up the publisher and subscriber, relied on the initial copy of the data, and let things run. Both of them will survive a restart, and thanks to the replication slot, they will keep going as soon as they come online.&lt;&#x2F;p&gt;
&lt;p&gt;But what if you need to perform maintenance or do regular things like a schema update on the subscriber or the publisher? Sometimes you might need to &lt;strong&gt;temporarily stop the replication&lt;&#x2F;strong&gt;. PostgreSQL makes this straightforward with subscription management, allowing you to stop consuming the changes.&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&lt;&#x2F;span&gt;&lt;span&gt; SUBSCRIPTION my_subscription &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DISABLE&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;As we hinted earlier, when disabled, the subscription stops consuming changes from the publisher, while keeping the replication slot. This means:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;No new changes are applied on the subscriber.&lt;&#x2F;li&gt;
&lt;li&gt;WAL files will start to accumulate on the publisher (since the replication slot holds its position).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;While disabled, you can&#x27;t really check the status from the subscriber, as only the replication slot has the data. You can use the query we mentioned above on the publisher to check the status.&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    slot_name,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    active,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; wal_retained&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_replication_slots;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;When ready, you can resume the subscription.&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&lt;&#x2F;span&gt;&lt;span&gt; SUBSCRIPTION my_subscription &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ENABLE&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;coordinating-schema-changes&quot;&gt;Coordinating Schema Changes&lt;a class=&quot;zola-anchor&quot; href=&quot;#coordinating-schema-changes&quot; aria-label=&quot;Anchor link for: coordinating-schema-changes&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;As we mentioned earlier, since logical replication does not automatically replicate DDL changes, you need to coordinate schema updates manually. Here&#x27;s a basic overview of how to perform such a change, step by step.&lt;&#x2F;p&gt;
&lt;p&gt;First, disable the replication on the subscriber and apply your schema changes.&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 replication&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&lt;&#x2F;span&gt;&lt;span&gt; SUBSCRIPTION my_subscription &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DISABLE&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;--- apply changes on the subscriber&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; products &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ADD&lt;&#x2F;span&gt;&lt;span&gt; COLUMN category_id &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: #F97583;&quot;&gt;CREATE INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; products_by_category&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; products(category_id);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Only then can you apply the changes to the publisher.&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 TABLE&lt;&#x2F;span&gt;&lt;span&gt; products &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ADD&lt;&#x2F;span&gt;&lt;span&gt; COLUMN category_id &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: #F97583;&quot;&gt;CREATE INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; products_by_category&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; products(category_id);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And resume the replication.&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&lt;&#x2F;span&gt;&lt;span&gt; SUBSCRIPTION my_subscription &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ENABLE&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;If you applied the schema change to the publisher first, any new data using the new column would fail to replicate to the subscriber that doesn&#x27;t have the column yet.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;handling-incompatible-changes&quot;&gt;Handling Incompatible Changes&lt;a class=&quot;zola-anchor&quot; href=&quot;#handling-incompatible-changes&quot; aria-label=&quot;Anchor link for: handling-incompatible-changes&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;PostgreSQL&#x27;s default conflict resolution is very simple. When a conflict occurs, &lt;strong&gt;logical replication stops&lt;&#x2F;strong&gt;, and the subscription enters an error state. For example, if you consider a product row already present on the subscriber, you will see something like this in the log files:&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;ERROR: duplicate key value violates unique constraint &amp;quot;products_pkey&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;DETAIL: Key (id)=(452) already exists.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;CONTEXT:  processing remote data for replication origin &amp;quot;pg_16444&amp;quot; during message type &amp;quot;INSERT&amp;quot; for replication target relation &amp;quot;public.products&amp;quot; in transaction 783, finished at 0&#x2F;1E020E8&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;LOG:  background worker &amp;quot;logical replication apply worker&amp;quot; (PID 457) exited with exit code 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;As you can see, logical replication stopped with an error code and won&#x27;t continue until you manually resolve the conflict. To do so, you need to identify the conflicting data and decide what to do with it.&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;-- option 1: remove the conflicting data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DELETE FROM&lt;&#x2F;span&gt;&lt;span&gt; products &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;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 452&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;-- option 2: update the data to match the expected state&lt;&#x2F;span&gt;&lt;&#x2F;span&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; products &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET name =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;New Product&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, price &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 29&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;99&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;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 452&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;Only after the conflict is resolved can the logical replication continue. This has been only a very simple example of a conflict that might arise during logical replication. Other examples might involve:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Data problems, constraint violations, or type mismatch&lt;&#x2F;li&gt;
&lt;li&gt;Schema conflicts (missing or renamed table&#x2F;column)&lt;&#x2F;li&gt;
&lt;li&gt;Permissions issues or row-level security&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;In all those cases, you still need to go and fix the problem before logical replication can resume.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wrap-up&quot;&gt;Wrap Up&lt;a class=&quot;zola-anchor&quot; href=&quot;#wrap-up&quot; aria-label=&quot;Anchor link for: wrap-up&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Logical replication in PostgreSQL opens up powerful possibilities beyond traditional physical replication. We have covered the foundations that can get you started with setting up your own logical replication environment, including understanding the differences between physical and logical replication, configuring publishers and subscribers, and managing core components like publications, subscriptions, and replication slots.&lt;&#x2F;p&gt;
&lt;p&gt;This article is part of the upcoming guide &lt;a href=&quot;&#x2F;guides&#x2F;mastering-logical-replication&#x2F;&quot;&gt;Mastering Logical Replication in PostgreSQL&lt;&#x2F;a&gt;. If you are interested in the topic, please consider subscribing to get the latest articles as they are published. Or you can continue to &lt;a href=&quot;&#x2F;posts&#x2F;logical-replication-beyond-the-basics&#x2F;&quot;&gt;Part2: Beyond The Basics of Logical Replication&lt;&#x2F;a&gt; available now.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>PostgreSQL Service Connections</title>
        <published>2025-05-15T00:00:00+00:00</published>
        <updated>2025-05-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/postgresql-service-definition/"/>
        <id>https://boringsql.com/posts/postgresql-service-definition/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/postgresql-service-definition/">&lt;p&gt;There are so many ways to connect to PostgreSQL. One of my favourite, yet
underutilised is using service definition, the feature of any application using
the &lt;code&gt;libpq&lt;&#x2F;code&gt; library. In this article we will explore what service definition is,
where and how it can make your life with PostgreSQL much easier.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;postgresql-connection-methods&quot;&gt;PostgreSQL connection methods&lt;a class=&quot;zola-anchor&quot; href=&quot;#postgresql-connection-methods&quot; aria-label=&quot;Anchor link for: postgresql-connection-methods&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;PostgreSQL offers several methods to establish connections to databases, each
with its own benefits and use cases:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Providing individual parameters like host, port, database, username, and password directly&lt;&#x2F;li&gt;
&lt;li&gt;Using a URI format like postgresql:&#x2F;&#x2F;username:password@hostname:port&#x2F;dbname&lt;&#x2F;li&gt;
&lt;li&gt;Using environment variables PGHOST, PGPORT, PGDATABASE, etc.&lt;&#x2F;li&gt;
&lt;li&gt;Command line arguments&lt;&#x2F;li&gt;
&lt;li&gt;And using pre-defined connection profiles stored in the service connection file(s)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;While the first four methods are widely known and used, service definitions remain somewhat of a hidden gem despite offering significant advantages in terms of security, maintainability, and convenience.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;service-definition-and-format&quot;&gt;Service definition and format&lt;a class=&quot;zola-anchor&quot; href=&quot;#service-definition-and-format&quot; aria-label=&quot;Anchor link for: service-definition-and-format&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;libpq-pgservice.html&quot;&gt;Connection service
file&lt;&#x2F;a&gt; allows
defining either per-user or system-wide connections (if both exist, the user one
will take precedence). The service file uses INI format and employs the full
list of &lt;code&gt;libpq&lt;&#x2F;code&gt; parameters to configure the service. A sample file looks like:&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;[mydb]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;host=localhost&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;port=5432&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;user=some_admin&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;password=password123&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dbname=my_db&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You can consult the list of parameters, together with their description,
directly using &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;libpq-connect.html#LIBPQ-PARAMKEYWORDS&quot;&gt;the
documentation&lt;&#x2F;a&gt;.
All you need to do is place the file in the correct location, which is: - either
user-specific &lt;code&gt;~&#x2F;.pg_service.conf&lt;&#x2F;code&gt; - or system-wide pg_service.conf in the
PostgreSQL&#x27;s configuration directory&lt;&#x2F;p&gt;
&lt;p&gt;and try it directly 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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;psql service=mydb&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;why-use-service-definitions&quot;&gt;Why use service definitions?&lt;a class=&quot;zola-anchor&quot; href=&quot;#why-use-service-definitions&quot; aria-label=&quot;Anchor link for: why-use-service-definitions&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;With all other methods available to connect to PostgreSQL, why even bother with
service definition? The primary benefit is the level of separation. While
creating connection strings&#x2F;DNS and environment variables is easy, they all
suffer from the same fundamental problem: tight coupling between your
application and database connection details.&lt;&#x2F;p&gt;
&lt;p&gt;Service definitions break this coupling by extracting connection parameters into
a standardised configuration format that any libpq-based application can
reference. Imagine using a single service definition across development, test,
and production environments. All it takes is to define:&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;service=my_app_db&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and supply the correct service definition for the target environment. The
application remains unaware of the underlying connection details, creating a
clean abstraction layer with clear service name identification.&lt;&#x2F;p&gt;
&lt;p&gt;Another aspect is re-usability. While your application might rely on an easy to
change configuration mechanism, you can easily spot the difference between these
two commands:&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;pg_dump&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --schema-only -h&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; my-app-db.internal&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -p 5432 -U&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; some_user&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; application&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.dump&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: #9ECBFF;&quot;&gt; service=my_app_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.dump&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The same applies to even internal mechanisms inside the running PostgreSQL
(&lt;code&gt;dblink&lt;&#x2F;code&gt; being a prime example - see below). This simplification extends to
database migration, replication setup, and monitoring configuration. The
consistency of service-based connections reduces errors and streamlines
operational procedures.&lt;&#x2F;p&gt;
&lt;p&gt;The benefits increase with improved security. Having a service definition file
stored outside your project GIT repository significantly reduces accidental
credentials sneaking inside the repository. Separation (which you can further
increase by use of &lt;code&gt;passfile&lt;&#x2F;code&gt; instead of &lt;code&gt;password&lt;&#x2F;code&gt;) extends flexibility, while
improving the security baseline.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;using-service-definitions&quot;&gt;Using Service Definitions&lt;a class=&quot;zola-anchor&quot; href=&quot;#using-service-definitions&quot; aria-label=&quot;Anchor link for: using-service-definitions&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Here are some ways to use service definitions:&lt;&#x2F;p&gt;
&lt;p&gt;With DSN keywords:&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;service=my_app_db&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;service=my_app_db application_name=myapp&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In connection URLs:&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;postgresql:&#x2F;&#x2F;&#x2F;?service=my_app_db&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;postgresql:&#x2F;&#x2F;&#x2F;?service=my_app_db&amp;amp;application_name=myapp&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Via environment variables:&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;PGSERVICE=my_app_db&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Given that &lt;code&gt;libpq&lt;&#x2F;code&gt; is the foundation of most programming language drivers, the same is applicable almost across the board:&lt;&#x2F;p&gt;
&lt;p&gt;In Go:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;go&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;&#x2F;&#x2F; Go&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;db, err&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; :=&lt;&#x2F;span&gt;&lt;span&gt; sql.&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;postgres&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;service=myservice&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;&#x2F;&#x2F; Go (pgx)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;conn, err&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; :=&lt;&#x2F;span&gt;&lt;span&gt; pgx.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Connect&lt;&#x2F;span&gt;&lt;span&gt;(context.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Background&lt;&#x2F;span&gt;&lt;span&gt;(),&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;service=my_app_db&amp;quot;&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;Same with Python:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# Python (with psycopg2)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;conn&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; psycopg2.connect(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;service=my_app_db&amp;quot;&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;Java:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;&#x2F;&#x2F; Java&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;System.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;setProperty&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;quot;org.postgresql.pgservicefile&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: #9ECBFF;&quot;&gt;    &amp;quot;&#x2F;path&#x2F;to&#x2F;pg_service.conf&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;Connection conn&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; DriverManager.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;getConnection&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;jdbc:postgresql:&#x2F;&#x2F;&#x2F;?service=my_app_db&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Not forgetting PHP:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;php&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;&#x2F;&#x2F; PHP&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$conn&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; pg_connect&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;service=my_app_db&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;&#x2F;&#x2F; PHP PDO&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$pdo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = new&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; PDO&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;pgsql:service=my_app_db&amp;quot;&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;Or Rust:&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 style=&quot;color: #6A737D;&quot;&gt;&#x2F;&#x2F; Rust&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; (client, connection)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&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;    tokio_postgres&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;connect&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;service=my_app_db&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; NoTls&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;.await?&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;Unfortunately, the services definitions are not available in drivers&#x2F;modules not
based on &lt;code&gt;libpq&lt;&#x2F;code&gt;, which includes, for example, Node&#x27;s pg module.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;using-service-definition-inside-postgresql&quot;&gt;Using service definition inside PostgreSQL&lt;a class=&quot;zola-anchor&quot; href=&quot;#using-service-definition-inside-postgresql&quot; aria-label=&quot;Anchor link for: using-service-definition-inside-postgresql&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;A notable use case for service definition is within PostgreSQL itself.
Hardcoding configuration details - either directly or using idempotent schema
files - is not particularly fun. Service definition significantly improves
configuration management in these cases.&lt;&#x2F;p&gt;
&lt;p&gt;This is the case for &lt;code&gt;dblink&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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SELECT * FROM dblink(&amp;#39;service=my_app_db&amp;#39;, &amp;#39;SELECT user_id, email FROM users&amp;#39;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;As well as FDW configuration:&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 SERVER foreign_server&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  FOREIGN DATA WRAPPER postgres_fdw&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  OPTIONS (service &amp;#39;my_app_db&amp;#39;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;While &lt;code&gt;postgres_fdw&lt;&#x2F;code&gt; naturally supports service definition, being based on
&lt;code&gt;libpq&lt;&#x2F;code&gt;, please note this is an exception and you can&#x27;t expect it for other
foreign data wrappers.&lt;&#x2F;p&gt;
&lt;p&gt;Similar to that, you need to consider the validity of the credentials, as
&lt;code&gt;libpq&lt;&#x2F;code&gt; reads the service definition at connection time, not at definition time.
I.e., for Postgres FDW:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Service name is stored on &lt;code&gt;CREATE SERVER&lt;&#x2F;code&gt;
execution&lt;&#x2F;li&gt;
&lt;li&gt;Service definition is read when connection is established (foreign
table accessed for the first time or service connection is validated)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Similar for &lt;code&gt;dblink&lt;&#x2F;code&gt; where the definition is read each time it establishes a new
connection. Don&#x27;t forget the existing connections are not affected.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;partial-service-definitions&quot;&gt;Partial Service Definitions&lt;a class=&quot;zola-anchor&quot; href=&quot;#partial-service-definitions&quot; aria-label=&quot;Anchor link for: partial-service-definitions&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The flexibility offered by service definition does not end there. The &lt;code&gt;libpq&lt;&#x2F;code&gt;
library follows a specific order when determining connection parameters:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Command-line parameters take highest precedence&lt;&#x2F;li&gt;
&lt;li&gt;Environment variables come next&lt;&#x2F;li&gt;
&lt;li&gt;Service definitions provide the next level&lt;&#x2F;li&gt;
&lt;li&gt;Default values are used as a last resort&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;This hierarchy enables you to define only some parameters in the service definition:&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;[mydb]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;host=localhost&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;port=5432&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dbname=my_db&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;With no credentials defined, you can later present the specific keywords (like
credentials in this example) using other means:&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;PGUSER=user1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;PGPASSWORD=pass1 psql service=my_db&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The same applies to connection strings&#x2F;DSNs, etc. This allows you to use a &quot;best
of both worlds&quot; approach, combining the consistency of service definition with
the flexibility to override it.&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;Service definitions represent a powerful but often overlooked feature of
PostgreSQL&#x27;s connection system. They provide a clean separation of connection
details from application code, enhance security by centralising credential
management, and simplify connection management across different environments and
applications.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Time to Better Know The Time in PostgreSQL</title>
        <published>2025-04-06T00:00:00+00:00</published>
        <updated>2025-04-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/know-the-time-in-postgresql/"/>
        <id>https://boringsql.com/posts/know-the-time-in-postgresql/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/know-the-time-in-postgresql/">&lt;p&gt;To honor the name of the site (boringSQL) let&#x27;s deep dive into a topic which might sound obvious, but it might be never ending source of surprises and misunderstanding.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;simple-things&quot;&gt;Simple things&lt;a class=&quot;zola-anchor&quot; href=&quot;#simple-things&quot; aria-label=&quot;Anchor link for: simple-things&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;We can start with the simple statement like&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 style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2025-03-30 00:30&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; t;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       t&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;2025-03-30 00:30&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;which gives you (and I do hope that&#x27;s not a big surprise) a simple text literal, rather than anything to do with date and time. As soon as you use the string literal in queries similar to&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; events &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; start_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;2025-03-30 00:30&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;PostgreSQL will implicitly convert the string into timestamp without time zone. The reason why it can happen is the fact &lt;code&gt;start_at&lt;&#x2F;code&gt; is most likely timestamp based field, allowing automatic cast to match the column&#x27;s data type. Which is unlike the query&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 style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2025-03-30 01:00&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; -&lt;&#x2F;span&gt;&lt;span&gt; interval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;15 minutes&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;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ERROR:  invalid input syntax for type interval: &amp;quot;2025-03-30 01:00&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;which will simply not work, as PostgreSQL can&#x27;t perform automatic cast (which as we will cover later might be surprising behavior, but that&#x27;s how things are). The correct way to get this example query to work is to use either one of two ways (both functionally equivalent but different notation).&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;-- PostgreSQL cast notation&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 style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2025-03-30 01:00&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamp -&lt;&#x2F;span&gt;&lt;span&gt; interval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;15 minutes&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- SQL standard explicit type notation&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 timestamp&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2025-03-03 01:00&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; -&lt;&#x2F;span&gt;&lt;span&gt; interval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;15 minutes&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;postgresql-timestamp-vs-timestamptz&quot;&gt;PostgreSQL timestamp vs timestamptz&lt;a class=&quot;zola-anchor&quot; href=&quot;#postgresql-timestamp-vs-timestamptz&quot; aria-label=&quot;Anchor link for: postgresql-timestamp-vs-timestamptz&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The next possible source of confusion when working with time in PostgreSQL is presence of two distinct data types:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;timestamp&lt;&#x2F;code&gt; (or &lt;code&gt;timestamp without time zone&lt;&#x2F;code&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;timestamptz&lt;&#x2F;code&gt; (or &lt;code&gt;timestamp with time zone&lt;&#x2F;code&gt;)
Despite what the names suggest, &lt;strong&gt;the key difference isn&#x27;t whether they store timezone information&lt;&#x2F;strong&gt;, but rather how they handle it during storage and retrieval.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;blockquote&gt;
&lt;p&gt;IMPORTANT: before we cover the details, remember to always use &lt;code&gt;timestamptz&lt;&#x2F;code&gt;. As official &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;wiki.postgresql.org&#x2F;wiki&#x2F;Don%27t_Do_This&quot;&gt;Don&#x27;t Do This&lt;&#x2F;a&gt; page shows, using &lt;code&gt;timestamp&lt;&#x2F;code&gt; is like storing picture of the clock, instead of point in time. And while this article might use &lt;code&gt;timestamp&lt;&#x2F;code&gt; it&#x27;s purely for demonstration purposes.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;All you need to remember is the fact &lt;code&gt;timestamp&lt;&#x2F;code&gt; &lt;strong&gt;stores the exact datetime value as entered&lt;&#x2F;strong&gt; without any timezone context or adjustments. It&#x27;s essentially a snapshot of a calendar date and wall clock time, disconnected from any particular geographic location.&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;-- this stores what you provided&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 style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2025-03-30 01:30&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamp&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;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      timestamp&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;2025-03-30 01:30:00&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;On the other hand &lt;code&gt;timestamptz&lt;&#x2F;code&gt; &lt;strong&gt;normalizes all input to UTC internally&lt;&#x2F;strong&gt; and then converts values to the session&#x27;s timezone for display. This provides geographic context to your datetime values.&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;-- this converts your input to UTC based on your session timezone&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 style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2025-03-30 01:30&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz&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;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      timestamptz&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;2025-03-30 01:30:00+01&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And this is where potential confusion might start. Let&#x27;s take this 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;-- With UTC timezone&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; timezone &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;UTC&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: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2025-03-30 01:30&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz&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;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      timestamptz&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; 2025-03-30 01:30:00+00&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;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- With Tokyo timezone&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; timezone &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;Asia&#x2F;Tokyo&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: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2025-03-30 01:30&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz&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;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      timestamptz&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; 2025-03-30 01:30:00+09&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;which gives you correct representation based on the session timezone. This comes in handy when we add the actual storage.&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;-- Create table and view with Berlin timezone&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; timezone &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;Europe&#x2F;Berlin&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: #F97583;&quot;&gt;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; time_flies&lt;&#x2F;span&gt;&lt;span&gt;(moment &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamp with time zone&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;INSERT INTO&lt;&#x2F;span&gt;&lt;span&gt; time_flies &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;2025-03-30 00:30&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: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; moment &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; time_flies;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         moment&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; 2025-03-30 00:30:00+01&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;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- View the same data with New York timezone&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; timezone &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;America&#x2F;New_York&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: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; moment &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; time_flies;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         moment&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; 2025-03-29 19:30:00-04&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This demonstrates the advantage of using automatic time zone conversion, allowing you to avoid not only time-zone bugs, but also DST related issues.
While most of us might be fast asleep during our local DST changes, it&#x27;s no fun to account for the time difference in a wrong. Using &lt;code&gt;timestamptz&lt;&#x2F;code&gt; is sure way how to avoid it. Let&#x27;s take an example of recent (at the time of writing the article) DST 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;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Set Berlin timezone (during DST change)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; timezone &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;Europe&#x2F;Berlin&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Using timestamp (without timezone awareness)&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 style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2025-03-30 01:30&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamp +&lt;&#x2F;span&gt;&lt;span&gt; interval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;45 minutes&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; end_time;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      end_time&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; 2025-03-30 02:15:00&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;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Using timestamptz (with timezone awareness)&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 style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2025-03-30 01:30&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz +&lt;&#x2F;span&gt;&lt;span&gt; interval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;45 minutes&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; end_time;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        end_time&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; 2025-03-30 03:15:00+02&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;at-time-zone&quot;&gt;AT TIME ZONE&lt;a class=&quot;zola-anchor&quot; href=&quot;#at-time-zone&quot; aria-label=&quot;Anchor link for: at-time-zone&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Another powerful but potentially confusing operator in PostgreSQL is &lt;code&gt;AT TIME ZONE&lt;&#x2F;code&gt; which allows you to convert timestamps between different time zones, but its behavior differs depending on the input data type.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;When applied to timestamp (without time zone)&lt;&#x2F;strong&gt;
When you apply AT TIME ZONE to a timestamp, PostgreSQL &lt;strong&gt;interprets the input as being in the specified time zone&lt;&#x2F;strong&gt; and converts it to a timestamptz:&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;-- First, set UTC timezone&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; timezone&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;UTC&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Interpret &amp;#39;2025-03-30 01:30&amp;#39; as if it were in the &amp;#39;Europe&#x2F;Paris&amp;#39; time zone&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 style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2025-03-30 01:30&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamp AT TIME ZONE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Europe&#x2F;Paris&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;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        timezone&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; 2025-03-30 00:30:00+00&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;What it does is effectively &quot;Take this wall clock time, consider it as being in the specified time zone, and give me the corresponding moment in time (as a timestamptz)&quot;. The representation here is based on session time zone settings.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;When applied to timestamptz (with time zone)&lt;&#x2F;strong&gt;
Conversely, when you apply AT TIME ZONE to a timestamptz, PostgreSQL &lt;strong&gt;converts the timestamp to the specified time zone and returns a timestamp without time zone&lt;&#x2F;strong&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: #6A737D;&quot;&gt;-- Set timezone&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; timezone&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;America&#x2F;Port_of_Spain&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Convert the timestamptz to how it would appear on wall clocks in Tokyo&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 style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2025-03-30 01:30+01:00&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz AT TIME ZONE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Asia&#x2F;Tokyo&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;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        timezone&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; 2025-03-30 09:30:00&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This operation says: &quot;Take this moment in time and show me what wall clock time it would be in the specified time zone.&quot;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Practical usage&lt;&#x2F;strong&gt;
Probably the only use when correctly using &lt;code&gt;timestamps&lt;&#x2F;code&gt; is to provide &quot;wall clock&quot; representation of the times at various time zones at once for (view) representational purposes.&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;-- What time is the company-wide meeting in various offices?&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 style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;2025-03-30 15:00&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz AS&lt;&#x2F;span&gt;&lt;span&gt; meeting_time_utc,&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;2025-03-30 15:00&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz AT TIME ZONE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Europe&#x2F;London&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS&lt;&#x2F;span&gt;&lt;span&gt; london_office_time,&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;2025-03-30 15:00&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz AT TIME ZONE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Asia&#x2F;Tokyo&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS&lt;&#x2F;span&gt;&lt;span&gt; tokyo_office_time;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    meeting_time_utc    | london_office_time  |  tokyo_office_time&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; 2025-03-30 15:00:00+02 | 2025-03-30 14:00:00 | 2025-03-30 22:00:00&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It&#x27;s important to note that in API based clients, it&#x27;s always better to represent full time notation and leave the clients with the representational layer. Similar to that, unless you need to work with multiple time zones, and you depend on the correct local time representation it&#x27;s always better to use local session &lt;code&gt;timezone&lt;&#x2F;code&gt; setting instead of using &lt;code&gt;AT TIME ZONE&lt;&#x2F;code&gt; operator. This applies for both time input and output.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Common mistake&lt;&#x2F;strong&gt;
Let&#x27;s consider the example where you might consider &quot;magically casting&quot; already stored time using &lt;code&gt;AT TIME ZONE&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: #6A737D;&quot;&gt;-- Setup example&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; timezone&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;Europe&#x2F;Berlin&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: #F97583;&quot;&gt;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; different_moments&lt;&#x2F;span&gt;&lt;span&gt;(moment1 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz&lt;&#x2F;span&gt;&lt;span&gt;, moment2 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz&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;INSERT INTO&lt;&#x2F;span&gt;&lt;span&gt; different_moments (moment1) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;values&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;2025-03-30 01:30&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Apply double time zone conversion&lt;&#x2F;span&gt;&lt;&#x2F;span&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; different_moments &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; moment2 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; moment1 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AT TIME ZONE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Europe&#x2F;Berlin&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AT TIME ZONE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;America&#x2F;New_York&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- View the results&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; different_moments;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        moment1         |        moment2&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; 2025-03-30 01:30:00+01 | 2025-03-30 07:30:00+02&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Unless you are aware of the implicit conversion between &lt;code&gt;timestamps&lt;&#x2F;code&gt; and &lt;code&gt;timestamp&lt;&#x2F;code&gt; and vice-versa you might be set for a lot of surprises. In this particular case the first conversion performed redundant conversion and removed the time zone offset. While second &quot;forced&quot; it to NYC time, while the subsequent select rendered it in local session timezone.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;timestamps-and-the-storage&quot;&gt;Timestamps and the storage&lt;a class=&quot;zola-anchor&quot; href=&quot;#timestamps-and-the-storage&quot; aria-label=&quot;Anchor link for: timestamps-and-the-storage&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;While you will use &lt;code&gt;timestamp with time zone&lt;&#x2F;code&gt; from now on, PostgreSQL still comes with several nuances related to the way it can store the data. First lesser known feature might be timestamp precision. Let&#x27;s consider following table.&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;-- Table with various timestamp precision specifications&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 TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; precision&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;	t1 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamp with time zone&lt;&#x2F;span&gt;&lt;span&gt;,        &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- default precision (6)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	t2 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamp&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; with time zone&lt;&#x2F;span&gt;&lt;span&gt;,     &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- seconds precision&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	t3 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamp&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; with time zone&lt;&#x2F;span&gt;&lt;span&gt;,     &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- centiseconds precision&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	t4 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamp&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; with time zone&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;      -- 10 microseconds precision&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;Effectively the &lt;code&gt;timestamp&lt;&#x2F;code&gt; comes with default precision of 6 digits (microseconds), but you can specify anywhere from 0 to 6 for both &lt;code&gt;timestamp&lt;&#x2F;code&gt; and &lt;code&gt;timestamptz&lt;&#x2F;code&gt; types. At the same time it does not have any impact on the storage (8 bytes) regardless the precision specified.&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;-- Insert the same timestamp into all columns&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 precision VALUES&lt;&#x2F;span&gt;&lt;span&gt; (current_timestamp, current_timestamp, current_timestamp, current_timestamp);&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 * FROM precision&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;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;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;t1 | 2025-04-03 21:19:20.952354+02&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;t2 | 2025-04-04 21:19:21+02&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;t3 | 2025-04-04 21:19:20.95+02&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;t4 | 2025-04-04 21:19:20.9524+02&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Other than precision there are some other interesting aspects of the timestamps storage in PostgreSQL:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;It has &lt;strong&gt;finite range&lt;&#x2F;strong&gt; of &lt;code&gt;timestamptz &#x27;4713-01-01 BC&#x27;&lt;&#x2F;code&gt; and &lt;code&gt;timestamptz &#x27;294276-12-31&#x27;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;and at the same time &lt;strong&gt;supports positive and negative infinity&lt;&#x2F;strong&gt; with &lt;code&gt;&#x27;infinity&#x27;::timestamptz&lt;&#x2F;code&gt; and &lt;code&gt;&#x27;-infinity&#x27;::timestamptz&lt;&#x2F;code&gt; that might provide alternative to &lt;code&gt;NULL&lt;&#x2F;code&gt; for open ended intervals. If you choose to use infinity instead of &lt;code&gt;NULL&lt;&#x2F;code&gt; values, you can test whatever the provided date&#x2F;timestamp is finite value or not using &lt;code&gt;isfinite(timestamp)&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;getting-the-current-time&quot;&gt;Getting the current time&lt;a class=&quot;zola-anchor&quot; href=&quot;#getting-the-current-time&quot; aria-label=&quot;Anchor link for: getting-the-current-time&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Not many developers are aware that there are different ways to get the current datetime in SQL. These methods not only differ in syntax but also in their underlying logic.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;current-timestamp-vs-now&quot;&gt;CURRENT_TIMESTAMP vs NOW()&lt;a class=&quot;zola-anchor&quot; href=&quot;#current-timestamp-vs-now&quot; aria-label=&quot;Anchor link for: current-timestamp-vs-now&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;strong&gt;CURRENT_TIMESTAMP&lt;&#x2F;strong&gt; is part of the SQL Standard and interestingly, it&#x27;s defined as both a &quot;time-varying variable&quot; and a &quot;datetime value function&quot;. In PostgreSQL, you can use it with or without parentheses, making both these forms valid:&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; CURRENT_TIMESTAMP;&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 style=&quot;color: #79B8FF;&quot;&gt; CURRENT_TIMESTAMP&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: #6A737D;&quot;&gt;-- (2) specifies precision&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Along with CURRENT_TIMESTAMP, the SQL Standard also defines &lt;code&gt;CURRENT_TIME&lt;&#x2F;code&gt; and &lt;code&gt;CURRENT_DATE&lt;&#x2F;code&gt; for working with individual time and date components respectively.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;NOW()&lt;&#x2F;code&gt;, while not part of the SQL Standard, is a widely used alternative across many database systems. Unlike CURRENT_TIMESTAMP, it&#x27;s strictly a function and therefore always requires parentheses when called. Its straightforward syntax - &lt;code&gt;NOW()&lt;&#x2F;code&gt; - makes it a popular choice among developers.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;transaction-behavior&quot;&gt;Transaction Behavior&lt;a class=&quot;zola-anchor&quot; href=&quot;#transaction-behavior&quot; aria-label=&quot;Anchor link for: transaction-behavior&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Both &lt;code&gt;NOW()&lt;&#x2F;code&gt; and &lt;code&gt;CURRENT_TIMESTAMP&lt;&#x2F;code&gt; share the same transaction behavior: they return the timestamp from the start of the current transaction, not the moment of execution:&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;BEGIN&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;SELECT NOW&lt;&#x2F;span&gt;&lt;span&gt;();                     &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Returns transaction start time&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(&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: #6A737D;&quot;&gt;-- Wait 5 seconds&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 NOW&lt;&#x2F;span&gt;&lt;span&gt;();                     &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Still returns the same time&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;COMMIT&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 behavior ensures data consistency within transactions but might be unexpected when measuring elapsed time.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;alternative-time-functions&quot;&gt;Alternative Time Functions&lt;a class=&quot;zola-anchor&quot; href=&quot;#alternative-time-functions&quot; aria-label=&quot;Anchor link for: alternative-time-functions&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;When you need the actual current time regardless of transaction status, &lt;code&gt;CLOCK_TIMESTAMP()&lt;&#x2F;code&gt; is your best choice. It returns the precise moment of execution, making it particularly useful for measuring real elapsed time. Here&#x27;s how it differs from &lt;code&gt;NOW()&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;BEGIN&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;SELECT&lt;&#x2F;span&gt;&lt;span&gt; pg_sleep(&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT NOW&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; transaction_time,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       CLOCK_TIMESTAMP()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS&lt;&#x2F;span&gt;&lt;span&gt; actual_time;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;COMMIT&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;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       transaction_time        |          actual_time&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; 2025-04-03 19:28:25.310254+00 | 2025-04-03 19:28:26.311917+00&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Another useful function is &lt;code&gt;STATEMENT_TIMESTAMP()&lt;&#x2F;code&gt;, which captures the time when the current statement began executing. This differs subtly from &lt;code&gt;CLOCK_TIMESTAMP()&lt;&#x2F;code&gt;, which gives you the exact moment of function execution. The difference becomes clear in this 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: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pg_sleep(&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;    STATEMENT_TIMESTAMP()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS statement&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;    CLOCK_TIMESTAMP()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS&lt;&#x2F;span&gt;&lt;span&gt; wall_time;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; pg_sleep |           statement           |          wall_time&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;          | 2025-04-03 19:32:15.984459+00 | 2025-04-03 19:32:17.98661+00&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;These timestamp functions serve different purposes and are invaluable when you need to measure transaction timing or analyze statement-level performance. Each provides a different perspective on time within your database operations.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;intervals&quot;&gt;Intervals&lt;a class=&quot;zola-anchor&quot; href=&quot;#intervals&quot; aria-label=&quot;Anchor link for: intervals&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Guess if you have worked with time in PostgreSQL you have come across with intervals. You probably just entered the it using something like &lt;code&gt;interval &#x27;1 year 2 months 3 days 4 hours&#x27;&lt;&#x2F;code&gt;. Technical that&#x27;s representation driven by setting &lt;code&gt;intervalstyle&lt;&#x2F;code&gt; which (as in this case) is set to &lt;code&gt;postgres_verbose&lt;&#x2F;code&gt;. But you have much more options.&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;-- PostgreSQL default style&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; intervalstyle &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;postgres&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: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; interval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;1 year 2 months 3 days 4 hours&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;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           interval&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; 1 year 2 mons 3 days 04:00:00&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;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- SQL standard style&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; intervalstyle &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;sql_standard&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: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; interval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;1 year 2 months 3 days 4 hours&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;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     interval&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; +1-2 +3 +4:00:00&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;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- ISO 8601 style&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; intervalstyle &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;iso_8601&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: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; interval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;1 year 2 months 3 days 4 hours&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;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  interval&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; P1Y2M3DT4H&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;With the session &lt;code&gt;intervalstyle&lt;&#x2F;code&gt; you only change the interval format representation, not the actual value.
Nevertheless the interval representation you might run into an interval definition that might not longer make it obvious (both in input or output) to interpret. That&#x27;s where function &lt;code&gt;justify_interval&lt;&#x2F;code&gt; comes in play - helping you to normalize time expression into conventional formats.&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;-- Normalize complex interval&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; justify_interval(interval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;15 months 40 days 30 hours&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;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        justify_interval&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; @ 1 year 4 mons 11 days 6 hours&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;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Practical example: normalize project durations&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;    project_name,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    total_hours &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; hours&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS&lt;&#x2F;span&gt;&lt;span&gt; raw_duration,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    justify_interval(interval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;1 hour&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span&gt; total_hours) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; normalized_duration&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;&#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;VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Database Migration&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1850&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: #9ECBFF;&quot;&gt;&amp;#39;API Development&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;720&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: #9ECBFF;&quot;&gt;&amp;#39;UI Redesign&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;340&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;    AS&lt;&#x2F;span&gt;&lt;span&gt; projects(project_name, total_hours);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    project_name    | raw_duration |   normalized_duration&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; Database Migration | 1850 hours   | @ 2 mons 17 days 2 hours&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; API Development    | 720 hours    | @ 1 mon&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; UI Redesign        | 340 hours    | @ 14 days 4 hours&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;One of the important specific with internal normalization is the fact it uses 30-days time periods as a month definition.&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;-- Month definition in normalize intervals&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; justify_interval(interval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;30 days&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;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; justify_interval&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; @ 1 mon&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Which might be understandable, but you need to beware of the edge cases when using days and months intervals.&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;-- Edge case comparison: month vs 30 days&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 style=&quot;color: #F97583;&quot;&gt;	date&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2025-01-31&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span&gt; interval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;1 month&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; example1,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;	date&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2025-01-31&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span&gt; interval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;30 days&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; example2;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;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;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;example1 | 2025-02-28 00:00:00&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;example2 | 2025-03-02 00:00:00&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And one last thing, before wrapping up the interval - you can (same as with dates&#x2F;timestamp) get specific parts using &lt;code&gt;extract&lt;&#x2F;code&gt; 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: #6A737D;&quot;&gt;-- Extract specific parts from intervals&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;    extract(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;days from&lt;&#x2F;span&gt;&lt;span&gt; interval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;1 year 3 months 21 days&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS days&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;    extract(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;hours from&lt;&#x2F;span&gt;&lt;span&gt; interval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;25:30:45&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS hours&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;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; days | hours&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; 21   | 25&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;time-ranges&quot;&gt;Time ranges&lt;a class=&quot;zola-anchor&quot; href=&quot;#time-ranges&quot; aria-label=&quot;Anchor link for: time-ranges&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;While working with timestamp comes natural, it&#x27;s time ranges which often feel like ugly ducklings. And quite unfairly so - timestamp ranges in PostgreSQL are powerful yet underutilized features that elegantly solve common time-based challenges. PostgreSQL offers dedicated range types that are perfect for modeling time periods, reservations, schedules, and any situation where you need to track intervals with defined start and end points:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tsrange&lt;&#x2F;code&gt; - range of timestamp without time zone&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;tstzrange&lt;&#x2F;code&gt; range of timestamp with time zone
And to follow the earlier advice - just as you should default to &lt;code&gt;timestamps&lt;&#x2F;code&gt;, always use &lt;code&gt;tstzrange&lt;&#x2F;code&gt; for time ranges unless you have VERY specific reason for it.
Timestamp ranges aren&#x27;t merely convenient syntax - they provide a complete set of operations for determining relationships between time periods and enable powerful constraints that handle complex business rules without reinventing the wheel.
Timestamp ranges can be created using the range constructor syntax or string syntax.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&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;-- Range constructor syntax&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; tstzrange(&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;2025-04-01 09:00:00+02&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz&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;2025-04-01 17:30:00+02&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz&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;[)&amp;#39;&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;AS&lt;&#x2F;span&gt;&lt;span&gt; workday;&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;-- String syntax&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 style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;[2025-04-01 09:00:00+02, 2025-04-01 17:30:00+02)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; workday;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                       workday&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[&amp;quot;2025-04-01 07:00:00+00&amp;quot;,&amp;quot;2025-04-01 15:30:00+00&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The default boundary notation for timestamp ranges is &lt;code&gt;[)&lt;&#x2F;code&gt; - lower bound inclusive (includes the start time) and upper bound exclusive (excludes the end time).
For time-based applications, this convention is particularly valuable. Consider scheduling consecutive one-hour meetings from 9:00 to 10:00 and 10:00 to 11:00. With &lt;code&gt;[)&lt;&#x2F;code&gt; notation, these are represented as &lt;code&gt;[09:00, 10:00)&lt;&#x2F;code&gt; and &lt;code&gt;[10:00, 11:00)&lt;&#x2F;code&gt;, creating perfect adjacency without overlap or gaps. If you used inclusive bounds &lt;code&gt;[09:00, 10:00]&lt;&#x2F;code&gt; and &lt;code&gt;[10:00, 11:00]&lt;&#x2F;code&gt;, the moment 10:00 would technically belong to both ranges - a logical impossibility for scheduling. This natural alignment with how we conceptualize time slots makes &lt;code&gt;[)&lt;&#x2F;code&gt; notation the default and recommended choice for most of the application use cases.
Timestamp ranges support numerous operations for checking relationships:&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;-- 1. Check if a timestamp is within a range&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 style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;[2025-04-01 09:00, 2025-04-01 17:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange @&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;2025-04-01 12:30&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamptz AS&lt;&#x2F;span&gt;&lt;span&gt; is_during_workday;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; is_during_workday&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; t&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;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- 2. Check if two ranges overlap&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 style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;[2025-04-01 09:00, 2025-04-01 12:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange &amp;amp;&amp;amp;&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;[2025-04-01 11:00, 2025-04-01 14:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; meetings_overlap;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; meetings_overlap&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; t&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;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- 3. Extract time range bounds&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 style=&quot;color: #79B8FF;&quot;&gt;    lower&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[2025-04-01 09:00, 2025-04-01 17:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; start_time,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    upper&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[2025-04-01 09:00, 2025-04-01 17:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; end_time;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        start_time        |         end_time&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; 2025-04-01 09:00:00+00   | 2025-04-01 17:00:00+00&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;As well as obvious manipulations:&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;-- 1. Merge adjacent ranges&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 style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;[2025-04-01 09:00, 2025-04-01 12:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&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;[2025-04-01 12:00, 2025-04-01 17:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; full_day;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                           full_day&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; [&amp;quot;2025-04-01 09:00:00+00&amp;quot;,&amp;quot;2025-04-01 17:00:00+00&amp;quot;)&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;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- 2. Find intersection between overlapping ranges&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 style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;[2025-04-01 09:00, 2025-04-01 14:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&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;[2025-04-01 12:00, 2025-04-01 17:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; overlap_period;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                        overlap_period&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; [&amp;quot;2025-04-01 12:00:00+00&amp;quot;,&amp;quot;2025-04-01 14:00:00+00&amp;quot;)&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;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- 3. Find gap between non-overlapping ranges&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 style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;[2025-04-01 09:00, 2025-04-01 11:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&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;[2025-04-01 14:00, 2025-04-01 17:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; gap;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                             gap&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; [&amp;quot;2025-04-01 11:00:00+00&amp;quot;,&amp;quot;2025-04-01 14:00:00+00&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And if you followed the previously mentioned logic, you wouldn&#x27;t double guess &lt;code&gt;tstzrange&lt;&#x2F;code&gt; handling of the DST transitions.&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;-- Testing DST transition handling&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; timezone &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;Europe&#x2F;Berlin&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: #F97583;&quot;&gt;SELECT&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;[2025-03-30 01:00:00, 2025-03-30 03:00:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; dst_transition_range,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    upper&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[2025-03-30 01:00:00, 2025-03-30 03:00:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&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;    lower&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[2025-03-30 01:00:00, 2025-03-30 03:00:00)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::tstzrange) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; actual_duration;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                dst_transition_range                 | actual_duration&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; [&amp;quot;2025-03-30 01:00:00+01&amp;quot;,&amp;quot;2025-03-30 03:00:00+02&amp;quot;) | @ 1 hour&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The area where time ranges win in almost all scenarios is indexing. If you consider a manual implementation of the range using &lt;code&gt;start_at&lt;&#x2F;code&gt; and &lt;code&gt;end_at&lt;&#x2F;code&gt; columns you typically rely on B-tree indexes. While they might work for simple queries on just one of the values, they fall short for time ranges. Consider this 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;-- Traditional approach with two columns and B-tree indexes&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; events_columns&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; start_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;lt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2025-04-01 17:00&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  AND&lt;&#x2F;span&gt;&lt;span&gt; end_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;2025-04-01 09:00&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;For this query, PostgreSQL might use one of the indexes if it&#x27;s selective enough, but the fundamental problem remains: each B-tree index can only efficiently filter on its own column. The planner might use the start_at index to find events starting before 17:00, or the end_at index to find events ending after 09:00, but it can&#x27;t use both indexes simultaneously to find the intersection.
With range types, we can use &lt;strong&gt;GiST&lt;&#x2F;strong&gt; (Generalized Search Tree) indexes that are specifically designed for range operations:&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;-- Create specialized GiST index for time range operations&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; idx_events_range&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; events_range &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;USING&lt;&#x2F;span&gt;&lt;span&gt; GIST(time_period);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This allows you to transform the sample query into:&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;-- Range-based approach using the &amp;amp;&amp;amp; (overlap) operator&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 * FROM&lt;&#x2F;span&gt;&lt;span&gt; events_range&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; time_period &amp;amp;&amp;amp; tstzrange(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;2025-04-01 09:00&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;2025-04-01 17:00&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;The performance gap between these approaches is substantial – range queries with GiST indexes typically &lt;strong&gt;outperform&lt;&#x2F;strong&gt; the traditional approach by 2-10x on large datasets. This difference becomes even more pronounced as your data grows.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;time-to-wrap-up&quot;&gt;Time to wrap up&lt;a class=&quot;zola-anchor&quot; href=&quot;#time-to-wrap-up&quot; aria-label=&quot;Anchor link for: time-to-wrap-up&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Working with time in PostgreSQL might seem straightforward on the surface, but as we&#x27;ve explored, it&#x27;s filled with nuances that can make or break your application&#x27;s reliability. The key takeaways from this deep dive should be:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Always use timestamptz instead of timestamp&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Be intentional about time zone handling&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Leverage PostgreSQL&#x27;s specialized time tools&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Mind the edge cases&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Time handling remains one of the most deceptively complex aspects of software engineering, but PostgreSQL offers a robust toolkit for managing it effectively. By understanding these &quot;boring&quot; but essential concepts, you&#x27;ll avoid common pitfalls and build applications that handle time with confidence and precision.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>VIEW inlining in PostgreSQL</title>
        <published>2025-02-08T00:00:00+00:00</published>
        <updated>2025-02-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/view-inlining/"/>
        <id>https://boringsql.com/posts/view-inlining/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/view-inlining/">&lt;p&gt;Database VIEWs are powerful tools that often don&#x27;t get the attention they deserve when building database-driven applications. They make our database work easier in several ways:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;They let us reuse common query patterns instead of writing them over and over&lt;&#x2F;li&gt;
&lt;li&gt;They give us a place to define business rules once and use them everywhere&lt;&#x2F;li&gt;
&lt;li&gt;They help us write cleaner, more organized queries&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Let&#x27;s see how this works with a practical example. Imagine we want to work with &lt;em&gt;active users&lt;&#x2F;em&gt; - users who have used the application within the last 7 days. Instead of writing this condition in every query, we can define a view:&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 VIEW&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; active_users&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS&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 style=&quot;color: #F97583;&quot;&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&gt; users;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	last_login &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; current_date &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span&gt; INTERVAL &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;7 days&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;Now we can easily use this view whenever we need active users. For example, if we want to find active users from Germany:&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	user_id&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; active_users&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	country &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;Germany&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;While this simple example shows how views can make developers&#x27; lives easier by organizing and reusing common logic, there&#x27;s more to the story. In this article, we&#x27;ll explore something even more interesting: how PostgreSQL can optimize these views through a process called &quot;inlining&quot; - making them not just convenient, but fast too.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-is-view-inlining&quot;&gt;What is VIEW inlining&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-is-view-inlining&quot; aria-label=&quot;Anchor link for: what-is-view-inlining&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;When you use a view, it acts like a building block in SQL queries. Taking our previous example, PostgreSQL effectively transforms the query by replacing the view with its underlying subquery.&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	user_id&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; (&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 style=&quot;color: #F97583;&quot;&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&gt; users&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;	WHERE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;		last_login &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; current_date &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span&gt; INTERVAL &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;7 days&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;) active users&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	country &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;Germany&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;While we write the query using &lt;code&gt;active_users&lt;&#x2F;code&gt; VIEW, PostgreSQL query planner will see it as an opportunity to optimize it further. Instead of treating the sub-query as separate step (and retrieving large subset of the users), it effectively transforms the query and &lt;strong&gt;inlines&lt;&#x2F;strong&gt; the view into the query itself. Behind the scenes, PostgreSQL will execute the query similar to:&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	user_id&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; users&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	last_login &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; current_date &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span&gt; INTERVAL &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;7 days&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;	AND&lt;&#x2F;span&gt;&lt;span&gt; country &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;Germany&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;The &lt;strong&gt;inlining process&lt;&#x2F;strong&gt; allows the PostgreSQL query planner to optimize the entire query as a single unit. This allows it to select the best indexes, possible join strategies and other aspects together, rather than having to execute the individual parts separately and then filtering the data that won&#x27;t be otherwise used.  This is exactly what VIEWs are for - we get the best of both worlds - clean, modular code for developers and optimal performance for the database.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;inlining-in-action&quot;&gt;Inlining in action&lt;a class=&quot;zola-anchor&quot; href=&quot;#inlining-in-action&quot; aria-label=&quot;Anchor link for: inlining-in-action&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The easiest way how to preview the VIEW inlining is to run EXPLAIN&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; Seq Scan on users  (cost=0.00..19.20 rows=1 width=4)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Filter: ((country = &amp;#39;Germany&amp;#39;::text) AND (last_login &amp;gt; (CURRENT_DATE - &amp;#39;7 days&amp;#39;::interval)))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The Filter part here is the one showing how the filtering conditions from both the query and the view got merged together by the query planner.&lt;&#x2F;p&gt;
&lt;p&gt;This works especially well with complex queries involving joins, aggregations and filters. PostgreSQL can optimize entire query chain as one unit instead of executing individual pieces separately.&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 VIEW&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; order_analytics&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS&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 style=&quot;color: #79B8FF;&quot;&gt;    o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;customer_id&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;    c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;country&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;    DATE_TRUNC(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;month&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;created_at&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;as month&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;    COUNT&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;span style=&quot;color: #F97583;&quot;&gt;as&lt;&#x2F;span&gt;&lt;span&gt; orders,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    SUM&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;total_amount&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; revenue&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; orders o&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;JOIN&lt;&#x2F;span&gt;&lt;span&gt; customers c &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; c&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;customer_id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GROUP BY&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;2&lt;&#x2F;span&gt;&lt;span&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;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;When you use this VIEW in the query&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; customer_id, revenue&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; order_analytics&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; country &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;Germany&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AND month &amp;gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;2024-01-01&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AND&lt;&#x2F;span&gt;&lt;span&gt; revenue &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; 1000&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 will see how PostgreSQL effectively inlines the filter conditions where they belong.&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;HashAggregate  (cost=205.97..207.97 rows=200 width=16)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Group Key: o.customer_id, c.country, (date_trunc(&amp;#39;month&amp;#39;::text, o.created_at))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Filter: (sum(o.total_amount) &amp;gt; 1000)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   -&amp;gt;  Hash Join  (cost=16.12..185.25 rows=1235 width=20)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         Hash Cond: (o.customer_id = c.id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         -&amp;gt;  Seq Scan on orders o&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-01-01&amp;#39;::date)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         -&amp;gt;  Hash&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;               -&amp;gt;  Seq Scan on customers c&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                     Filter: (country = &amp;#39;Germany&amp;#39;::text)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This works especially well with increasing query complexity. Particulary involving joins and further filters. PostgreSQL can optimize entire query chain as one unit instead of executing individual pieces separately.&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 VIEW completed_orders AS&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SELECT&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   o.id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   o.customer_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   o.total_amount,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   o.created_at,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   o.status,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   p.name as product_name,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   c.email,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   c.country&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;FROM orders o&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;JOIN customers c ON c.id = o.customer_id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;JOIN order_items oi ON oi.order_id = o.id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;JOIN products p ON p.id = oi.product_id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;WHERE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   o.status = &amp;#39;completed&amp;#39;;&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&gt;SELECT&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;FROM completed_orders&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;WHERE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	country = &amp;#39;Germany&amp;#39;;&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&gt;CREATE INDEX idx_orders_customer_id ON orders(customer_id); CREATE INDEX idx_order_items_order_id ON order_items(order_id); CREATE INDEX idx_order_items_product_id ON order_items(product_id);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;as demonstrated in the query plan&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; Nested Loop  (cost=0.30..19.70 rows=1 width=176)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   -&amp;gt;  Nested Loop  (cost=0.15..13.52 rows=1 width=148)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         Join Filter: (o.id = oi.order_id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         -&amp;gt;  Nested Loop  (cost=0.15..12.43 rows=1 width=144)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;               -&amp;gt;  Seq Scan on orders o  (cost=0.00..1.05 rows=1 width=80)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                     Filter: (status = &amp;#39;completed&amp;#39;::text)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;               -&amp;gt;  Index Scan using customers_pkey on customers c  (cost=0.15..8.17 rows=1 width=68)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                     Index Cond: (id = o.customer_id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                     Filter: (country = &amp;#39;Germany&amp;#39;::text)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         -&amp;gt;  Seq Scan on order_items oi  (cost=0.00..1.04 rows=4 width=8)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   -&amp;gt;  Index Scan using products_pkey on products p  (cost=0.15..6.17 rows=1 width=36)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         Index Cond: (id = oi.product_id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;planner-barriers&quot;&gt;Planner barriers&lt;a class=&quot;zola-anchor&quot; href=&quot;#planner-barriers&quot; aria-label=&quot;Anchor link for: planner-barriers&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;While PostgreSQL is very good at inlining views, certain operations create &quot;planner barrier&quot; that prevent the optimization. In case of views some of the conditions that will cause it are&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;DISTINCT ON operations&lt;&#x2F;li&gt;
&lt;li&gt;Window functions (OVER clauses)&lt;&#x2F;li&gt;
&lt;li&gt;Set operations (UNION&#x2F;INTERSECT&#x2F;EXCEPT)&lt;&#x2F;li&gt;
&lt;li&gt;More complex aggregations&lt;&#x2F;li&gt;
&lt;li&gt;Common Table Expressions that are materialized (either with MATERIALIZED hint or by the &lt;a href=&quot;&#x2F;posts&#x2F;good-cte-bad-cte&#x2F;&quot;&gt;decision of the query planner&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;Use of VOLATILE functions&lt;&#x2F;li&gt;
&lt;li&gt;And in some cases complex subqueries&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;When planner is not able to inline VIEWs it might lead to the unnecessary performance and resource impact. I.e. each sub query might materialize results separately, leading to the already retrieved rows to be discarded by another part of the query as one of the most common examples.&lt;&#x2F;p&gt;
&lt;p&gt;When using EXPLAIN this is demostated by one of the following options:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Subquery scan on...&lt;&#x2F;code&gt; nodes&lt;&#x2F;li&gt;
&lt;li&gt;Materialization nodes&lt;&#x2F;li&gt;
&lt;li&gt;Separate aggregation&#x2F;sorting steps before the main execution&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;As mentioned above the window function acts as such a planner barrier.&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 VIEW order_insights AS&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SELECT&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   o.customer_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   o.total_amount,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   o.created_at,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   ROW_NUMBER() OVER (PARTITION BY o.customer_id ORDER BY o.created_at) as order_sequence,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   SUM(o.total_amount) OVER (PARTITION BY o.customer_id ORDER BY o.created_at) as running_total&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;FROM orders o;&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&gt;EXPLAIN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SELECT * FROM order_insights&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;WHERE customer_id = 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;AND total_amount &amp;gt; 500;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Giving us easy to spot &lt;code&gt;Subquery scan&lt;&#x2F;code&gt; node.&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; Subquery Scan on order_insights  (cost=1.06..1.10 rows=1 width=84)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Filter: (order_insights.total_amount &amp;gt; &amp;#39;500&amp;#39;::numeric)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   -&amp;gt;  WindowAgg  (cost=1.06..1.08 rows=1 width=84)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         -&amp;gt;  Sort  (cost=1.06..1.06 rows=1 width=44)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;               Sort Key: o.created_at&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;               -&amp;gt;  Seq Scan on orders o  (cost=0.00..1.05 rows=1 width=44)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                     Filter: (customer_id = 1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;runtime-materialization-not-materialized-views&quot;&gt;Runtime Materialization (Not MATERIALIZED VIEWs)&lt;a class=&quot;zola-anchor&quot; href=&quot;#runtime-materialization-not-materialized-views&quot; aria-label=&quot;Anchor link for: runtime-materialization-not-materialized-views&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Let&#x27;s clear up a possible confusion - we&#x27;re not talking about CREATE MATERIALIZED VIEW here. This is about PostgreSQL&#x27;s runtime decision to cache view results in memory during query execution. Query planner might use explicit materialization when it needs to reference view results multiple times or prevent repeated expensive computations. The query planner shows this through a Materialize node:&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 VIEW&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; customer_summary&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS&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;    customer_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    COUNT&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;span style=&quot;color: #F97583;&quot;&gt;as&lt;&#x2F;span&gt;&lt;span&gt; orders,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    SUM&lt;&#x2F;span&gt;&lt;span&gt;(total_amount) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;as&lt;&#x2F;span&gt;&lt;span&gt; total_spent&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; orders&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GROUP BY&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;&#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&gt;EXPLAIN &lt;&#x2F;span&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 style=&quot;color: #79B8FF;&quot;&gt;    a&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;customer_id&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;    a&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;total_spent&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;    b&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;total_spent&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; other_customer_spent&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; customer_summary a&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;JOIN&lt;&#x2F;span&gt;&lt;span&gt; customer_summary b &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; b&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;total_spent&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; a&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;total_spent&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;Giving the perfect example of materialization.&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; Nested Loop  (cost=46.00..200.75 rows=3333 width=68)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   Join Filter: (b.total_spent &amp;gt; (sum(orders.total_amount)))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   -&amp;gt;  HashAggregate  (cost=23.00..24.25 rows=100 width=44)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         Group Key: orders.customer_id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         -&amp;gt;  Seq Scan on orders  (cost=0.00..18.00 rows=1000 width=15)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   -&amp;gt;  Materialize  (cost=23.00..25.75 rows=100 width=32)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         -&amp;gt;  Subquery Scan on b  (cost=23.00..25.25 rows=100 width=32)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;               -&amp;gt;  HashAggregate  (cost=23.00..24.25 rows=100 width=44)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                     Group Key: orders_1.customer_id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                     -&amp;gt;  Seq Scan on orders orders_1  (cost=0.00..18.00 rows=1000 width=15)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Next to the materialization - &lt;strong&gt;the materialized VIEWs&lt;&#x2F;strong&gt; (&lt;code&gt;CREATE MATERIALIZED VIEW&lt;&#x2F;code&gt;) are for practical purposes &quot;just another table&quot; they present an ultimate planner barrier. PostgreSQL must scan the materialized data directly, trading query flexibility for performance&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tips-for-writing-inlining-friendly-views&quot;&gt;Tips for writing inlining friendly VIEWs&lt;a class=&quot;zola-anchor&quot; href=&quot;#tips-for-writing-inlining-friendly-views&quot; aria-label=&quot;Anchor link for: tips-for-writing-inlining-friendly-views&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Well, there&#x27;s not a single &quot;right&quot; way how to write views. And don&#x27;t forget - VIEWs should make your database easier to work with, not harder. When designing views, focus on single responsibility - each view should handle one aspect of business logic rather than trying to optimize everything at once. Simple views are more likely to get inlined by PostgreSQL&#x27;s query planner.&lt;&#x2F;p&gt;
&lt;p&gt;There are several common patterns that prevent inlining&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Avoid window functions, distinct and set operations completely&lt;&#x2F;li&gt;
&lt;li&gt;Split complex logic into multiple views if possible&lt;&#x2F;li&gt;
&lt;li&gt;Minimize subquery usage&lt;&#x2F;li&gt;
&lt;li&gt;Consider materialized views for aggregations&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;View dependencies are a critical consideration. Even minor schema changes can trigger cascading view modifications across your database. Instead of creating deep view hierarchies, split complex logic into smaller, composable views. For critical views that many applications depend on, consider versioning (v1, v2) rather than modifying existing ones.&lt;&#x2F;p&gt;
&lt;p&gt;When in doubt, write the plain SQL first, then extract common patterns into views. This helps avoid overengineering and keeps your database schema maintainable.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>DELETEs are difficult</title>
        <published>2024-11-23T00:00:00+00:00</published>
        <updated>2024-11-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/deletes-are-difficult/"/>
        <id>https://boringsql.com/posts/deletes-are-difficult/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/deletes-are-difficult/">&lt;p&gt;Your database is ticking along nicely - until a simple DELETE brings it to its
knees. What went wrong? While we tend to focus on optimizing SELECT and INSERT
operations, we often overlook the hidden complexities of DELETE. Yet, removing
unnecessary data is just as critical. Outdated or irrelevant data can bloat your
database, degrade performance, and make maintenance a nightmare. Worse,
retaining some types of data without valid justification might even lead to
compliance issues.&lt;&#x2F;p&gt;
&lt;p&gt;At first glance, the DELETE command seems straightforward. Even the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;sql-delete.html&quot;&gt;PostgreSQL
documentation&lt;&#x2F;a&gt; provides
simple examples like:&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;DELETE FROM&lt;&#x2F;span&gt;&lt;span&gt; films &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; kind &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;lt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Musical&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: #F97583;&quot;&gt;DELETE FROM&lt;&#x2F;span&gt;&lt;span&gt; films;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;These queries might work effortlessly on your development machine, where only a
few hundred records exist. But what happens when you try running a similar
DELETE in production, where datasets are orders of magnitude larger?&lt;&#x2F;p&gt;
&lt;p&gt;In this article, we’ll uncover why DELETE operations
demand careful consideration and explore how to handle them effectively.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-really-happens-when-you-delete-the-data&quot;&gt;What really happens when you DELETE the data?&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-really-happens-when-you-delete-the-data&quot; aria-label=&quot;Anchor link for: what-really-happens-when-you-delete-the-data&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;At first glance, a DELETE query might seem straightforward. However, once the query is executed, a series of intricate steps occur:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Row Identification&lt;&#x2F;strong&gt;: Similar to a SELECT operation, the query identifies rows visible to the current transaction (considering MVCC) and checks for locks.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Lock Acquisition&lt;&#x2F;strong&gt;: The database acquires row-level exclusive locks to prevent other operations on the targeted rows.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;BEFORE DELETE trigger&lt;&#x2F;strong&gt;: If a BEFORE DELETE trigger is defined, it is executed at this point.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Marking Rows as Deleted&lt;&#x2F;strong&gt;: Instead of being physically removed, the rows are marked as deleted in the current transaction, rendering them invisible to future queries (depending on transaction isolation). If table has large data objects, TOAST table will have to be involved too.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Index Updates&lt;&#x2F;strong&gt;: The corresponding index entries are also marked for deletion (if applicable).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Cascaded Actions&lt;&#x2F;strong&gt;: Cascading operations, such as ON DELETE CASCADE, are performed on related tables.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;AFTER DELETE Trigger&lt;&#x2F;strong&gt;: If an AFTER DELETE trigger is defined, it is executed.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Write-Ahead Log (WAL)&lt;&#x2F;strong&gt;: Changes are recorded in the WAL-first at the row level, followed by index-level updates.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Only when the transaction is committed do these changes become permanent and visible to transactions starting afterward. However, even at this point, the data is &lt;strong&gt;not physically removed&lt;&#x2F;strong&gt;. This is how &lt;strong&gt;bloat&lt;&#x2F;strong&gt; is created.&lt;&#x2F;p&gt;
&lt;p&gt;Until the &lt;strong&gt;autovacuum&lt;&#x2F;strong&gt; process or a manual VACUUM operation reclaims the space, the “deleted” data remains. This leftover data contributes to bloat, which can degrade query performance over time.&lt;&#x2F;p&gt;
&lt;p&gt;The key question now is whether DELETEs are truly the hardest operation we can subject our database to. The answer? Quite possibly. While UPDATEs come close in complexity, they’re typically designed in ways that make them less challenging:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;UPDATEs usually modify only a limited number of columns, reducing the potential number of index updates lower.&lt;&#x2F;li&gt;
&lt;li&gt;Not all UPDATEs trigger a full row (COLD) update, where the old row is marked as dead and a new row is created. With careful table and query design, you can minimise these cases. HOT (Heap-Only Tuple) updates, for instance, are easier to achieve with fixed-length columns.&lt;&#x2F;li&gt;
&lt;li&gt;Unlike DELETEs, UPDATEs don’t trigger cascaded actions - they only involve triggers that are explicitly defined.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;and-then-comes-autovacuum&quot;&gt;And then comes AUTOVACUUM&lt;a class=&quot;zola-anchor&quot; href=&quot;#and-then-comes-autovacuum&quot; aria-label=&quot;Anchor link for: and-then-comes-autovacuum&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;When autovacuum kicks in (and you really want it to kick in) - typically triggered by thresholds for the number of dead tuples or changes to the table - a significant amount of work is required to clean them up. Let’s break it down step by step:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The process begins by scanning the table. While it’s not always a full table scan, the autovacuum checks the visibility map and pages where dead tuples might exist. This can happen incrementally, allowing even large tables to be processed - provided your autovacuum settings allow it to run frequently enough.&lt;&#x2F;li&gt;
&lt;li&gt;Each tuple is checked to ensure it’s no longer visible to any active or pending transactions.&lt;&#x2F;li&gt;
&lt;li&gt;Dead tuples that pass the visibility check are physically removed from the table.&lt;&#x2F;li&gt;
&lt;li&gt;Corresponding index entries for the removed tuples are updated.&lt;&#x2F;li&gt;
&lt;li&gt;The now-empty space is marked for reuse in future INSERT or UPDATE operations.&lt;&#x2F;li&gt;
&lt;li&gt;Table statistics are updated to reflect the current state, helping the query planner make better decisions.&lt;&#x2F;li&gt;
&lt;li&gt;The changes, including tuple removals and index updates, are logged in the Write-Ahead Log (WAL) for durability and replication.&lt;&#x2F;li&gt;
&lt;li&gt;If the table has TOASTed data (large objects), the associated TOAST tables are processed.&lt;&#x2F;li&gt;
&lt;li&gt;The visibility map is updated to mark cleaned pages as fully visible again.&lt;&#x2F;li&gt;
&lt;li&gt;Autovacuum resets thresholds to determine when the next vacuum operation should occur.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This process continues until it reaches the configured vacuum cost limit (in the case of autovacuum), at which point it pauses or stops. While autovacuum helps keep your database in check, it’s clear that reclaiming dead tuples is no small task - and it underscores why DELETE operations can have lasting effects on database performance.&lt;&#x2F;p&gt;
&lt;p&gt;While the &lt;code&gt;AUTOVACUUM&lt;&#x2F;code&gt; might sounds as a bad news, without it, your database would quickly become bloated with dead tuples, leading to degraded performance, slower queries, increased storage usage, and even the potential for out-of-disk errors as unused space cannot be reclaimed.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;further-considerations&quot;&gt;Further considerations&lt;a class=&quot;zola-anchor&quot; href=&quot;#further-considerations&quot; aria-label=&quot;Anchor link for: further-considerations&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;For what seems like a simple DELETE, a surprising amount of work has already taken place, but the complexity doesn’t stop there. DELETE operations can introduce additional challenges, particularly when replication, resource contention, or the size of the operation comes into play.&lt;&#x2F;p&gt;
&lt;p&gt;In environments with replication to hot standby or replicas, DELETEs become more time-sensitive. The transaction cannot complete until the corresponding WAL (Write-Ahead Log) records have been written to disk on the standby. This is a fundamental requirement for maintaining data consistency in high-availability setups, where at least one standby server is typically involved. Additionally, if the standby is actively serving read operations, it must account for DELETEs before confirming the changes, potentially introducing further delays.&lt;&#x2F;p&gt;
&lt;p&gt;The size of the DELETE operation also plays a critical role. Small DELETEs, such as removing a single row, tend to have minimal impact. However, as the size of the operation grows, so does the volume of WAL records generated. Large DELETEs can overwhelm the system, slowing down transactions and straining the replication process. Standby servers must work harder to process the incoming WAL stream, which can bottleneck performance if their throughput is insufficient.&lt;&#x2F;p&gt;
&lt;p&gt;Resource contention adds yet another layer of complexity, particularly for large DELETEs. Generating WAL records, handling regular transactional workloads, and running background processes can collectively push the system towards I&#x2F;O saturation. This creates competition for CPU and memory resources, leading to slower operations across the board.&lt;&#x2F;p&gt;
&lt;p&gt;Finally, once the data is marked for deletion, the autovacuum process must eventually step in to physically remove it. This introduces its own set of challenges, as autovacuum must deal with the same resource contention and I&#x2F;O demands, compounding the overall impact of the initial DELETE operation.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;soft-deletes-are-not-the-solution&quot;&gt;Soft-deletes are not the solution&lt;a class=&quot;zola-anchor&quot; href=&quot;#soft-deletes-are-not-the-solution&quot; aria-label=&quot;Anchor link for: soft-deletes-are-not-the-solution&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Soft-deletes might seem like an easy way to sidestep the complexities of
traditional DELETE operations. After all, updating a &lt;code&gt;deleted_at&lt;&#x2F;code&gt; field is
straightforward and unlikely to trigger a COLD update. However, soft-deletes are
not a true mechanism for data removal, and they come with their own set of
complications.&lt;&#x2F;p&gt;
&lt;p&gt;While soft-deletes can provide a simple way to implement “undo” functionality,
they raise serious questions about data consistency. For instance, do you only
mark the main entity as deleted, or do you also cascade the status to all
related records in referenced tables? Failing to cascade properly can leave your
database in an inconsistent state, making it difficult to maintain data
integrity.&lt;&#x2F;p&gt;
&lt;p&gt;Soft-deletes also require consideration in your application logic. Every query
must include appropriate filters to exclude “deleted” rows, which can complicate
query design and increase the risk of oversights. One missed filter could expose
data that should no longer be visible, leading to potential security or business
logic issues.&lt;&#x2F;p&gt;
&lt;p&gt;Finally, soft-deletes don’t solve the problem - they do merely postpone it. The
data is still in your database, consuming storage and potentially contributing
to performance degradation over time. Sooner or later, you’ll need to deal with
the actual removal of this data, bringing you back to the same challenges that
DELETEs pose in the first place.&lt;&#x2F;p&gt;
&lt;p&gt;At the time of writing this article we can only speculate how much will support
of temporal PRIMARY KEY and UNIQUE constriants in PostgreSQL 18 will transform
the balances in future. But given the complexity of the feature I wouldn&#x27;t bet
on it just yet.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;batching-is-the-answer&quot;&gt;Batching is the answer&lt;a class=&quot;zola-anchor&quot; href=&quot;#batching-is-the-answer&quot; aria-label=&quot;Anchor link for: batching-is-the-answer&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Giving PostgreSQL time to process and catch up with large-scale changes is critical when dealing with operations like DELETEs. The core issue here is the duration and magnitude of the transaction. &lt;strong&gt;The shorter the transaction and the fewer changes made, the better PostgreSQL can manage and reconcile those changes.&lt;&#x2F;strong&gt; This principle is universal across all database operations and underscores the importance of minimizing the impact of individual transactions.&lt;&#x2F;p&gt;
&lt;p&gt;While you can optimize certain aspects, like row identification (using indexes, clustering, or similar techniques), larger datasets demand a more strategic approach - batching. For example, deleting 1 million rows in a single transaction is a textbook case of what not to do. Instead, splitting the operation into smaller batches, such as deleting 10,000 rows across 100 iterations, is far more effective.&lt;&#x2F;p&gt;
&lt;p&gt;Will this method be faster than performing one massive DELETE? Likely not, especially if you include a wait time between batches to allow PostgreSQL to handle other workloads. However, the trade-off is worthwhile. By batching, you give PostgreSQL more breathing room to manage changes without overwhelming regular transactional workloads - unless, of course, you’ve scheduled dedicated maintenance time for the operation.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;how-to-batch-deletes&quot;&gt;How to Batch DELETEs&lt;a class=&quot;zola-anchor&quot; href=&quot;#how-to-batch-deletes&quot; aria-label=&quot;Anchor link for: how-to-batch-deletes&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;The easiest way to batch DELETEs is to use a subquery or a &lt;a href=&quot;&#x2F;posts&#x2F;good-cte-bad-cte&#x2F;&quot;&gt;Common Table
Expression (CTE)&lt;&#x2F;a&gt; to limit the number of rows affected in each iteration. For
example, instead of executing a bulk DELETE 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;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DELETE FROM&lt;&#x2F;span&gt;&lt;span&gt; films &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; kind &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;lt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Musical&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;You can break the operation into smaller chunks. Using a query like the
following, you can repeatedly delete rows in manageable batches (for instance,
using \watch in psql to automate iterations):&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;DELETE FROM&lt;&#x2F;span&gt;&lt;span&gt; films&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; ctid &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;&#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; ctid &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; films&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    WHERE&lt;&#x2F;span&gt;&lt;span&gt; kind &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;lt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Musical&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    LIMIT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 250&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;The use of &lt;code&gt;ctid&lt;&#x2F;code&gt; in this example is PostgreSQL system column which provides a
unique identifier for each row. By selecting &lt;code&gt;ctid&lt;&#x2F;code&gt; values in the subquery, you
can limit the number of rows affected in each iteration. This approach is more
efficient than using &lt;code&gt;LIMIT&lt;&#x2F;code&gt; directly in the main query, as it avoids the need
to re-scan the table for each batch.&lt;&#x2F;p&gt;
&lt;p&gt;If you don&#x27;t feel comfortable with &lt;code&gt;ctid&lt;&#x2F;code&gt; (which might deserve article on its
own) you can use regular lookup by primary key and &lt;code&gt;LIMIT&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;planning-for-autovacuum&quot;&gt;Planning for Autovacuum&lt;a class=&quot;zola-anchor&quot; href=&quot;#planning-for-autovacuum&quot; aria-label=&quot;Anchor link for: planning-for-autovacuum&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Batching alone doesn’t directly solve the issue of autovacuum catching up with the changes. You’ll need to plan for that separately. Adjusting autovacuum settings or triggering manual &lt;code&gt;VACUUM&lt;&#x2F;code&gt; and &lt;code&gt;VACUUM ANALYZE&lt;&#x2F;code&gt; runs can help manage bloat created during the DELETE process. However, disabling autovacuum is rarely advisable unless you’ve carefully planned for manual maintenance throughout the batch operations. Skipping this step risks leaving behind performance-impacting bloat that will require even more effort to address later.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;drop-whole-range-of-data-with-partitioning&quot;&gt;Drop whole range of data with partitioning&lt;a class=&quot;zola-anchor&quot; href=&quot;#drop-whole-range-of-data-with-partitioning&quot; aria-label=&quot;Anchor link for: drop-whole-range-of-data-with-partitioning&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Data that is naturally segmented - for example by time of the creation - makes it an excellent candidate for removal through partitioning. Partitioning allows you to bypass DELETE operations altogether by simply dropping or truncating the relevant partitions. This approach is far more efficient and avoids the overhead of scanning, locking, and marking rows as deleted, effectively eliminating the problem with the bloat.&lt;&#x2F;p&gt;
&lt;p&gt;While partitioning adds some complexity to schema design and query planning, it can provide significant performance benefits for DELETE-heavy workloads, especially when combined with automated partition management.&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;DELETE operations are often a source of unpleasant surprises - not
just by affecting performance and creating bloat, but by striking back at the
times we least expect. To handle them effectively, focus on strategies such as
batching, monitoring autovacuum, or leveraging partitioning for large datasets.
By considering DELETE operations during schema design, you can maintain an
efficient database, reduce maintenance headaches, and ensure it continues to run
smoothly as your data grows.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Text identifiers in PostgreSQL database design</title>
        <published>2024-11-09T00:00:00+00:00</published>
        <updated>2024-11-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/text-identifier-in-db-design/"/>
        <id>https://boringsql.com/posts/text-identifier-in-db-design/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/text-identifier-in-db-design/">&lt;p&gt;Whether you are designing a standalone application or a microservice, you will inevitably encounter the topic of sharing identifiers. Whether it’s URLs of web pages, RESTful API resources, JSON documents, CSV exports, or something else, the identifier of specific resources will be exposed.&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;&#x2F;orders&#x2F;123&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&#x2F;products&#x2F;345&#x2F;variants&#x2F;1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;While an identifier is just a number and does not carry any negative connotations, there are valid reasons why you might want to avoid exposing them. These reasons include:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Security and Data Exposure&lt;&#x2F;strong&gt;: Numerical identifiers are sequential and predictable, which can expose information about the underlying data source (e.g., the volume of the data) and provide a basis for ID enumeration.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Privacy and Confidentiality&lt;&#x2F;strong&gt;: Concerns may arise about concealing the volume of referenced data. For example, the number of customers, clients, or orders might be information a business prefers to keep private.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Non-descriptive Nature&lt;&#x2F;strong&gt;: Integers as identifiers can lead to confusion. An ID like 123 does not convey any additional information, making debugging edge cases more challenging.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;These and other reasons (like SEO optimisation) have led to the increased use of text-based identifiers. Their readability and versatility make them ideal for external data sharing.&lt;&#x2F;p&gt;
&lt;p&gt;However, in database (or data model) design, the advantages of text identifiers are often overshadowed by the problems they introduce. While text identifiers improve interoperability, they frequently come with performance and storage trade-offs. In contrast, integers are naturally faster and more efficient to process, resulting in lower storage requirements and faster indexing, sorting, and searching-tasks that computers are optimised for.&lt;&#x2F;p&gt;
&lt;p&gt;In this article, we will explore scenarios where using text identifiers directly in the database design might seem natural and discuss strategies for using them effectively.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;what-makes-text-identifiers-appealing&quot;&gt;What Makes Text Identifiers Appealing?&lt;a class=&quot;zola-anchor&quot; href=&quot;#what-makes-text-identifiers-appealing&quot; aria-label=&quot;Anchor link for: what-makes-text-identifiers-appealing&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Let’s be honest-text identifiers are popular for a reason. For humans, they are much more readable and, in some cases, add extra context. (Raise your hand if you don’t appreciate Heroku’s quirky and memorable names!) If chosen carefully, they can also be easier to recall.&lt;&#x2F;p&gt;
&lt;p&gt;Text identifiers can embed additional context. Consider the order number APAC-20241103-8237, which encodes both the region and the order date. Another popular reason for using text identifiers is their uniqueness in distributed environments.&lt;&#x2F;p&gt;
&lt;p&gt;They’re especially handy when people need to interact with them directly. For example, customers copying an order number from an email or support teams discussing an issue benefit from a readable, meaningful identifier. It’s simpler, more intuitive, and less likely to cause headaches when someone tries to recall or share it.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;when-an-identifier-isn-t-just-an-identifier&quot;&gt;When an Identifier Isn’t Just an Identifier&lt;a class=&quot;zola-anchor&quot; href=&quot;#when-an-identifier-isn-t-just-an-identifier&quot; aria-label=&quot;Anchor link for: when-an-identifier-isn-t-just-an-identifier&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Problems with text identifiers arise when they are used as natural keys in your data or database model. Despite their benefits, text identifiers often make poor primary keys for several reasons:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Context Changes&lt;&#x2F;strong&gt;: The additional context provided by text identifiers will likely change, necessitating updates. Despite assurances to the contrary, changes are inevitable.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Sorting Issues&lt;&#x2F;strong&gt;: Sorting text identifiers can be tricky, particularly with locale-based sorting or when numbers are embedded within the identifier (e.g., order1434 vs. order349).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The ultimate issue is efficiency:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Text identifiers typically require &lt;strong&gt;more storage space&lt;&#x2F;strong&gt; than numeric ones. Each character in a text field occupies more bytes than a simple integer, leading to larger database sizes and slower performance, especially during indexing or handling large datasets.&lt;&#x2F;li&gt;
&lt;li&gt;Databases are optimised for numeric operations, making &lt;strong&gt;searches, joins, and indexing on text fields inherently slower&lt;&#x2F;strong&gt;. This performance gap can significantly affect large datasets, impacting application efficiency.&lt;&#x2F;li&gt;
&lt;li&gt;Text identifiers complicate &lt;strong&gt;managing relationships between tables&lt;&#x2F;strong&gt;. The additional storage requirements affect not only the source table but also all referencing entities. Multiply this increased storage by the number of referencing tables, and you’ll get a sense of the overall impact.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;As databases grow, these issues become more pronounced. The combination of increased storage needs and slower operations contributes to &lt;strong&gt;database bloat&lt;&#x2F;strong&gt;, which can degrade system performance. Additionally, bloated indexes can mislead the query planner into making suboptimal choices, such as favouring sequential scans over index scans, complicating regular operations further.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;aren-t-uuids-the-solution-to-all-this&quot;&gt;Aren&#x27;t UUIDs the Solution to All This?&lt;a class=&quot;zola-anchor&quot; href=&quot;#aren-t-uuids-the-solution-to-all-this&quot; aria-label=&quot;Anchor link for: aren-t-uuids-the-solution-to-all-this&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;It depends. While UUIDs offer numerical representation and are excellent for unique key generation across distributed systems, they are not always the best choice:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;In comparison to BIGINT, there are few real-world scenarios that necessitate the full range of UUIDs. Premature optimisation most often isn’t worthwhile for most solutions.&lt;&#x2F;li&gt;
&lt;li&gt;UUIDs’ 16-byte (128-bit) storage size can be less efficient than many text identifiers. Even BIGINT, at 8 bytes (64 bits), is more storage-efficient. This inefficiency extends to indexes, joins, and other operations.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;ul&gt;
&lt;li&gt;A matter of opinion: UUIDs are plain ugly.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Moreover, different versions of UUIDs offer varying benefits. For example, UUIDv1 includes a timestamp component, making it somewhat sortable, while UUIDv4 is entirely random. Even sortable UUIDs may not offer significant advantages over more streamlined options like BIGINT or carefully structured text identifiers.&lt;&#x2F;p&gt;
&lt;p&gt;In most cases, the trade-offs in performance and readability make UUIDs less appealing unless their globally unique nature is essential across distributed systems.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;real-life-examples&quot;&gt;Real-Life Examples&lt;a class=&quot;zola-anchor&quot; href=&quot;#real-life-examples&quot; aria-label=&quot;Anchor link for: real-life-examples&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Let’s move beyond theory and explore practical examples:&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; sessions&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;    token &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT 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;    user_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT NOT NULL REFERENCES&lt;&#x2F;span&gt;&lt;span&gt; users(user_id),&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;&#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; products&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;    sku &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT 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;    label &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT 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;    ...&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;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; documents&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;    document_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT 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;    ...&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;In these cases, using text identifiers as primary keys might seem logical because:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;They serve as natural keys for the entity.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;ul&gt;
&lt;li&gt;They are likely propagated outside the service scope, hence needing storage anyway.&lt;&#x2F;li&gt;
&lt;li&gt;They are not expected to change.&lt;&#x2F;li&gt;
&lt;li&gt;The storage impact for a single table seems negligible.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;However, this logic falters under scrutiny. Text identifiers often propagate beyond the service scope, leading to pressure to update them. Updating primary keys is no trivial matter. Consider:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;While SKUs ideally never change, real-world scenarios like rebranding, product consolidation, or supplier changes can necessitate updates. Despite their appeal, SKUs are poor primary key candidates.&lt;&#x2F;li&gt;
&lt;li&gt;Randomly generated text identifiers (like session tokens) will… TBD.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The real issue arises when referencing these identifiers across tables:&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; session_logs&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;    log_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BIGINT GENERATED ALWAYS AS IDENTITY&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;    token &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT NOT NULL REFERENCES sessions&lt;&#x2F;span&gt;&lt;span&gt;(token),&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;&#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; product_reviews&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;    review_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;    product_sku &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT NOT NULL REFERENCES&lt;&#x2F;span&gt;&lt;span&gt; products(sku),&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;&#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; customer_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;    order_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT 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;TEXT NOT NULL REFERENCES&lt;&#x2F;span&gt;&lt;span&gt; customers(customer_id),&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;&#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; document_revisions&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;    revision_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;    document_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT NOT NULL REFERENCES&lt;&#x2F;span&gt;&lt;span&gt; documents(document_id),&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;In such scenarios, text identifiers can become problematic. Firstly, you &lt;strong&gt;lose the ability to modify them&lt;&#x2F;strong&gt;. Secondly, the &lt;strong&gt;increased storage requirements&lt;&#x2F;strong&gt; become evident. For instance, an additional 100 bytes per reference across a million records results in an extra 100 MB of storage for just one reference.&lt;&#x2F;p&gt;
&lt;p&gt;However, storage isn’t the biggest concern; &lt;strong&gt;indexing&lt;&#x2F;strong&gt; is. In PostgreSQL, indexing text fields - whether as primary or foreign keys - requires significantly more space than indexing numeric fields. This leads to bloated indexes, slower lookups, and more fragmented operations, particularly in queries relying on foreign key relationships. As a result, the query planner might resort to full table scans instead of index scans, further degrading performance.&lt;&#x2F;p&gt;
&lt;p&gt;These performance issues often remain undetected in development or testing environments but can cause significant disruptions in production.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;reintroducing-text-identifiers-effectively&quot;&gt;Reintroducing Text Identifiers Effectively&lt;a class=&quot;zola-anchor&quot; href=&quot;#reintroducing-text-identifiers-effectively&quot; aria-label=&quot;Anchor link for: reintroducing-text-identifiers-effectively&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;The goal of this post is not to discourage the use of text identifiers entirely but to highlight why they are unsuitable as primary keys. Here are some strategies to handle them effectively:&lt;&#x2F;p&gt;
&lt;h4 id=&quot;1-introduce-a-surrogate-key&quot;&gt;1. Introduce a Surrogate Key&lt;a class=&quot;zola-anchor&quot; href=&quot;#1-introduce-a-surrogate-key&quot; aria-label=&quot;Anchor link for: 1-introduce-a-surrogate-key&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h4&gt;
&lt;p&gt;One straightforward solution is to introduce a surrogate primary key, replacing references to text identifiers:&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; products&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;    product_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT GENERATED BY DEFAULT AS IDENTITY&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;    sku &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT 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;    label &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT 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;    ...&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;CREATE INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; products_by_sku&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; products(sku);&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; product_reviews&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;    review_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;    product_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT REFERENCES&lt;&#x2F;span&gt;&lt;span&gt; products(product_id),&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 approach maintains efficient retrieval by SKU via an index.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;2-use-mapping-tables-for-greater-flexibility&quot;&gt;2. Use Mapping Tables for Greater Flexibility&lt;a class=&quot;zola-anchor&quot; href=&quot;#2-use-mapping-tables-for-greater-flexibility&quot; aria-label=&quot;Anchor link for: 2-use-mapping-tables-for-greater-flexibility&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h4&gt;
&lt;p&gt;Mapping tables allow you to:&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;* 	Update identifiers without affecting the parent entity.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;* 	Maintain a history of text identifiers linked to a specific entity.&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;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; products&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;    product_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT GENERATED BY DEFAULT AS IDENTITY&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;    label &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT 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;    ...&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;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; product_skus&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;    product_sku_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT GENERATED BY DEFAULT AS IDENTITY&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;    product_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT REFERENCES&lt;&#x2F;span&gt;&lt;span&gt; products(product_id),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    sku &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT 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;    created_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TIMESTAMPTZ DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; CURRENT_TIMESTAMP,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    deleted_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TIMESTAMPTZ&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;CREATE UNIQUE INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; unique_product_skus&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; product_skus (product_id, sku) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; deleted_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IS NULL&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 approach accommodates changing SKUs without compromising data integrity. A related use case could be linking a mapping table to a product variant:&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; product_variants&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;    variant_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT GENERATED BY DEFAULT 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;    product_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT NOT NULL REFERENCES&lt;&#x2F;span&gt;&lt;span&gt; products(product_id),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    sku &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT 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;    name TEXT 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;    ...&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;h4 id=&quot;3-decompose-the-text-identifier-s-meaning&quot;&gt;3. Decompose the Text Identifier’s Meaning&lt;a class=&quot;zola-anchor&quot; href=&quot;#3-decompose-the-text-identifier-s-meaning&quot; aria-label=&quot;Anchor link for: 3-decompose-the-text-identifier-s-meaning&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h4&gt;
&lt;p&gt;Text identifiers often embed meaningful information, such as regions, dates, or categories. By decomposing the identifier, you can store this contextual data separately, improving both flexibility and performance.&lt;&#x2F;p&gt;
&lt;p&gt;For example, instead of storing an order identifier like ORD-EMEA-00789 directly, you can design a more robust schema:&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; 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;    order_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT GENERATED BY DEFAULT 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;    region_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT NOT NULL REFERENCES&lt;&#x2F;span&gt;&lt;span&gt; regions(region_id),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    order_date &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DATE 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;    ...&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;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; regions&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;    region_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT GENERATED BY DEFAULT 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 style=&quot;color: #F97583;&quot;&gt;    name TEXT 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;    code &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT 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;    ...&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; regions (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span&gt;, code) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&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: #9ECBFF;&quot;&gt;&amp;#39;Europe, the Middle East, and Africa&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;EMEA&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;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Asia Pacific&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;APAC&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;To&lt;&#x2F;span&gt;&lt;span&gt; generate a user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span&gt;friendly order &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;number&lt;&#x2F;span&gt;&lt;span&gt;, you can &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;create&lt;&#x2F;span&gt;&lt;span&gt; a &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&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 OR REPLACE FUNCTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; get_order_number&lt;&#x2F;span&gt;&lt;span&gt;(p_order_id &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 style=&quot;color: #F97583;&quot;&gt;RETURNS TEXT AS&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;DECLARE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    v_region_code &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&gt;    v_formatted_order_id &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&gt;    v_order_number &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;BEGIN&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 style=&quot;color: #79B8FF;&quot;&gt; r&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;code&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; INTO&lt;&#x2F;span&gt;&lt;span&gt; v_region_code&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; orders o&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    JOIN&lt;&#x2F;span&gt;&lt;span&gt; regions r &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;region_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; r&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;region_id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    WHERE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; o&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;order_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; p_order_id;&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;    IF NOT&lt;&#x2F;span&gt;&lt;span&gt; FOUND &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        RETURN 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;    END IF&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&gt;    v_formatted_order_id :&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; TO_CHAR(p_order_id, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;FM00000&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;    v_order_number :&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;ORD-&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span&gt; v_region_code &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;-&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span&gt; v_formatted_order_id;&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;    RETURN&lt;&#x2F;span&gt;&lt;span&gt; v_order_number;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;END&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;LANGUAGE&lt;&#x2F;span&gt;&lt;span&gt; plpgsql;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This method enables you to store and retrieve structured data efficiently while still providing a readable and meaningful identifier for external use.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;4-reversible-text-ids&quot;&gt;4. Reversible Text IDs&lt;a class=&quot;zola-anchor&quot; href=&quot;#4-reversible-text-ids&quot; aria-label=&quot;Anchor link for: 4-reversible-text-ids&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h4&gt;
&lt;p&gt;For scenarios requiring enhanced security or privacy, where identifiers should not be easily enumerable or predictable, you can use reversible text-based IDs. These allow you to present users with seemingly random text representations while maintaining efficient numerical storage internally.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;sqids.org&#x2F;&quot;&gt;Sqids&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; is a project that can help achieve this. It generates URL-friendly unique identifiers from numbers and can encode multiple numerical identifiers into a single string. Here’s an example using the Sqids project:&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;[42] -&amp;gt; JgaEBgznCpUZo3Kk&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[42, 430004] -&amp;gt; lTiYlvsGkh59m1PQ&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The generated identifiers are reversible with knowledge of the shared alphabet, allowing you to decode requests without hitting the database, which can be beneficial in high-throughput environments. This technique is particularly useful for user, account, or session identifiers, balancing the need for security with operational efficiency.&lt;&#x2F;p&gt;
&lt;p&gt;However, it’s crucial to remember that this is not a substitute for robust security practices. Proper authentication and authorisation mechanisms are still necessary to secure your application.&lt;&#x2F;p&gt;
&lt;p&gt;By thoughtfully integrating these strategies, you can leverage the benefits of text identifiers where appropriate while avoiding common pitfalls in database design. This balance ensures efficient, maintainable systems that meet both technical and business needs.&lt;&#x2F;p&gt;
&lt;h4 id=&quot;5-leverage-generate-columns&quot;&gt;5. Leverage Generate columns&lt;a class=&quot;zola-anchor&quot; href=&quot;#5-leverage-generate-columns&quot; aria-label=&quot;Anchor link for: 5-leverage-generate-columns&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h4&gt;
&lt;p&gt;In edge case scenarios, where keeping text identifiers with embedded context is necessary, &lt;strong&gt;generated columns&lt;&#x2F;strong&gt; in PostgreSQL can be a valuable feature. Since version 12, PostgreSQL allows defining columns whose values are automatically computed from other columns in the table. This ensures consistency without manual intervention.&lt;&#x2F;p&gt;
&lt;p&gt;For example, you can define a function to handle the formatting logic:&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 OR REPLACE FUNCTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; get_formatted_order_id&lt;&#x2F;span&gt;&lt;span&gt;(p_region_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT&lt;&#x2F;span&gt;&lt;span&gt;, p_order_id &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 style=&quot;color: #F97583;&quot;&gt;RETURNS TEXT AS&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;DECLARE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    v_region_code &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;BEGIN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    v_region_code :&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;= CASE&lt;&#x2F;span&gt;&lt;span&gt; p_region_id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        WHEN&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; THEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;EMEA&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        WHEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; THEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;APAC&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        ELSE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;OTHER&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    END&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;    RETURN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;ORD-&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span&gt; v_region_code &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;-&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span&gt; LPAD(p_order_id::&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: #79B8FF;&quot;&gt;5&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;0&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: #F97583;&quot;&gt;END&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;LANGUAGE&lt;&#x2F;span&gt;&lt;span&gt; plpgsql IMMUTABLE;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The function is marked IMMUTABLE because PostgreSQL requires generated columns to use only immutable functions-those guaranteed to return the same result for the same input every 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;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; 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;    order_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT GENERATED BY DEFAULT 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;    region_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT 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;    order_date &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DATE 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;    formatted_order_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT GENERATED ALWAYS AS&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;        get_formatted_order_id(region_id, order_id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ) STORED&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 setup ensures that &lt;code&gt;formatted_order_id&lt;&#x2F;code&gt; is automatically updated whenever &lt;code&gt;region_id&lt;&#x2F;code&gt; or &lt;code&gt;order_id&lt;&#x2F;code&gt; changes. The column is physically stored, enhancing read performance for frequently queried data.&lt;&#x2F;p&gt;
&lt;p&gt;Using generated columns can simplify maintaining consistency in derived values like formatted text identifiers. However, be mindful of their characteristics, such as:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Automated Updates&lt;&#x2F;strong&gt;: The system automatically recomputes the column’s value when the referenced columns change.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Immutability Requirements&lt;&#x2F;strong&gt;: Only immutable functions can be used, ensuring reliable and consistent computation.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;wrapping-up&quot;&gt;Wrapping up&lt;a class=&quot;zola-anchor&quot; href=&quot;#wrapping-up&quot; aria-label=&quot;Anchor link for: wrapping-up&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Text identifiers will stay with us, and that&#x27;s great. They’re human-readable, memorable, and can pack a lot of meaningful context into a simple string. They make external interactions smoother, whether it’s a customer referencing an order number or a support team tracing an issue. They even add a bit of charm and personality to otherwise sterile identifiers.&lt;&#x2F;p&gt;
&lt;p&gt;However it&#x27;s important to keep their usage in control. The nice rule I&#x27;ve heard is&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Use text identifiers when &lt;strong&gt;communicating externally&lt;&#x2F;strong&gt; - for example, in URLs or API responses&lt;&#x2F;li&gt;
&lt;li&gt;Internally, always rely on &lt;strong&gt;numerical IDs&lt;&#x2F;strong&gt; - surrogate keys like INT or BIGINT (even UUID if you go for planet dominanance) where necessary, to maintain database efficiency and integrity.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This approach allows you to leverage the strengths of text identifiers for external communication while keeping your database optimised for performance and scalability. By storing text identifiers in dedicated fields and using numeric primary keys for internal operations, you achieve a the right balance and do best to maintain the system performance.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>We need to talk about ENUMs</title>
        <published>2024-09-04T00:00:00+00:00</published>
        <updated>2024-09-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/postgresql-enums/"/>
        <id>https://boringsql.com/posts/postgresql-enums/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/postgresql-enums/">&lt;p&gt;Designing a database schema, whether for a new application or a new feature, always raises a lot of questions. The choices you make can have a big impact on how well your database performs and how easy it is to maintain and scale. Whether you’re just getting started with PostgreSQL or consider yourself a seasoned pro, it’s easy to rely on old habits or outdated advice. In this article, I want to take a fresh look at one of those topics that often sparks debate: the use of ENUMs in PostgreSQL.&lt;&#x2F;p&gt;
&lt;p&gt;I have to admit, not so long ago, I would advise &quot;don&#x27;t use ENUMs&quot; without thinking about it too much. Relying only on random articles and some personal but outdated experience, this had been my go-to answer for some years. And while there were several limitations of ENUMs in PostgreSQL in the (distant) past, the support has improved a long time ago.&lt;&#x2F;p&gt;
&lt;p&gt;The improvements are so long-standing that:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;PostgreSQL 9.1 (released in 2011) introduced the option to add new values to ENUMs &lt;strong&gt;without a table rewrite&lt;&#x2F;strong&gt;. From what I can say this fact alone remained the source of biggest misconception when thinking about ENUMs.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Renaming of values&lt;&#x2F;strong&gt; was added in version 10 (2017).&lt;&#x2F;li&gt;
&lt;li&gt;The ability to &lt;code&gt;ALTER TYPE ... ADD VALUE&lt;&#x2F;code&gt; in a transaction block was introduced in PostgreSQL 12 (2019).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;And new features coming (at the time of writing this article) with &lt;strong&gt;PostgreSQL 17&lt;&#x2F;strong&gt; allow the use of newly added values within the same transaction block (previously not possible without an explicit commit).&lt;&#x2F;p&gt;
&lt;p&gt;Therefore, I want to correct even myself and say—let&#x27;s give ENUMs another chance. This article will go into detail and help us correctly decide when it makes sense to use them or not.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-are-enums-implemented&quot;&gt;How are ENUMs Implemented?&lt;a class=&quot;zola-anchor&quot; href=&quot;#how-are-enums-implemented&quot; aria-label=&quot;Anchor link for: how-are-enums-implemented&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Every stored ENUM value occupies 4 bytes on disk. This is the size of the OID, representing the actual ENUM value, stored as a row within the &lt;code&gt;pg_enum&lt;&#x2F;code&gt; table. You can see this yourself by querying the table directly, but it will come by default with no data defined.&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;# select * from pg_catalog.pg_enum;&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&gt; oid | enumtypid | enumsortorder | enumlabel&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;(0 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;But once you define a new ENUM data type, 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;CREATE TYPE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; order_status&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS&lt;&#x2F;span&gt;&lt;span&gt; ENUM (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;new&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &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;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;processing&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;shipped&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &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;p&gt;You will get the actual OIDs.&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;# select * from pg_catalog.pg_enum;&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&gt;  oid   | enumtypid | enumsortorder | enumlabel&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; 211018 |    211016 |             1 | new&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 211020 |    211016 |             2 | pending&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 211022 |    211016 |             3 | processing&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 211024 |    211016 |             4 | shipped&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 211026 |    211016 |             5 | delivered&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The table straightforwardly identifies both the type and individual values, but also the &lt;code&gt;enumsortorder&lt;&#x2F;code&gt;, which is the foundation for &lt;code&gt;BEFORE&#x2F;AFTER&lt;&#x2F;code&gt; new value(s) definition.&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 TYPE&lt;&#x2F;span&gt;&lt;span&gt; order_status &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ADD VALUE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;cancelled&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; BEFORE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;shipped&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;Resulting in:&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;  oid   | enumtypid | enumsortorder | enumlabel&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; 211018 |    211016 |             1 | new&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 211020 |    211016 |             2 | pending&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 211022 |    211016 |             3 | processing&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 211024 |    211016 |             4 | shipped&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 211026 |    211016 |             5 | delivered&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 211027 |    211016 |           3.5 | cancelled&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;To confirm the actual storage size of the ENUM value, you can use a sample table, the previously defined type &lt;code&gt;order_status&lt;&#x2F;code&gt;, a single data row, and the &lt;code&gt;pg_column_size&lt;&#x2F;code&gt; 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;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; 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;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 style=&quot;color: #F97583;&quot;&gt;    status&lt;&#x2F;span&gt;&lt;span&gt; order_status &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&gt;    order_date &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; orders (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;status&lt;&#x2F;span&gt;&lt;span&gt;, order_date) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&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: #9ECBFF;&quot;&gt;&amp;#39;new&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;2024-09-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;p&gt;This gives you confirmation of the actual 4-byte storage.&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;# SELECT id, status, pg_column_size(status) AS status_size&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&gt; id | status | status_size&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;  1 | new    |           4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Returning to the &lt;code&gt;pg_enum&lt;&#x2F;code&gt; table, the structure of the table provides other important clues:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;enumlabel&lt;&#x2F;code&gt; is of type &lt;code&gt;name&lt;&#x2F;code&gt;, which is effectively a 63-byte varchar for storing system identifiers.&lt;&#x2F;li&gt;
&lt;li&gt;The &lt;code&gt;UNIQUE CONSTRAINT pg_enum_typid_label_index&lt;&#x2F;code&gt; prevents us from creating two values with the same name.&lt;&#x2F;li&gt;
&lt;li&gt;However, given the nature of &lt;code&gt;VARCHAR&lt;&#x2F;code&gt;, it is case-sensitive, allowing you to define &lt;code&gt;new&lt;&#x2F;code&gt; and &lt;code&gt;NEW&lt;&#x2F;code&gt; as two distinct values.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;life-with-enums&quot;&gt;Life with ENUMs&lt;a class=&quot;zola-anchor&quot; href=&quot;#life-with-enums&quot; aria-label=&quot;Anchor link for: life-with-enums&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Now that we understand how ENUMs are implemented, let&#x27;s review the lifecycle of such a data type in a typical database.&lt;&#x2F;p&gt;
&lt;p&gt;As demonstrated above, &lt;strong&gt;creation of new ENUMs&lt;&#x2F;strong&gt; is simple, and since version 12 (and expanded in 17), it&#x27;s possible even in transaction blocks, making it easier to work with database migration tools without needing to explicitly worry about transaction management.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Adding new values&lt;&#x2F;strong&gt; does not require a table rewrite and can be done without further hesitation. You can also safely &lt;strong&gt;rename existing values&lt;&#x2F;strong&gt;. Similarly, you can use ENUMs in arrays, indexes, or compare them as needed (based on their order).&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 TYPE name ADD VALUE&lt;&#x2F;span&gt;&lt;span&gt; [ IF NOT EXISTS ] new_enum_value [ { BEFORE | AFTER } neighbor_enum_value ]&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;ALTER TYPE name&lt;&#x2F;span&gt;&lt;span&gt; RENAME &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUE&lt;&#x2F;span&gt;&lt;span&gt; existing_enum_value &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; new_enum_value&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It&#x27;s also important to mention that ENUMs allow you to use a default value, 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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;status order_status NOT NULL DEFAULT &amp;#39;new&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Unfortunately, this is where the flexibility ends at the moment. There&#x27;s no easy way to remove or re-order existing values (please, don&#x27;t modify &lt;code&gt;pg_enum&lt;&#x2F;code&gt;—or any system tables, for that matter). The topic of removing existing ENUM value(s) can easily open up a whole discussion about the best way to &#x27;deprecate&#x27; and drop a value over time. The only suitable way to perform these operations is by creating a new type and altering the column. Please be aware of the ways described in &lt;strong&gt;&lt;a href=&quot;&#x2F;posts&#x2F;how-not-to-change-postgresql-column-type&#x2F;&quot;&gt;How not to change PostgreSQL column type&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt; will apply in this scenario.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;using-check-constraints-as-an-alternative-to-enums&quot;&gt;Using CHECK Constraints as an Alternative to ENUMs&lt;a class=&quot;zola-anchor&quot; href=&quot;#using-check-constraints-as-an-alternative-to-enums&quot; aria-label=&quot;Anchor link for: using-check-constraints-as-an-alternative-to-enums&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;While ENUMs are a powerful tool in PostgreSQL for enforcing a fixed set of values in a column, they come with the limitations mentioned above. An alternative approach to achieving similar functionality is to use a CHECK constraint with a TEXT column. This method offers more flexibility when managing the allowed values, especially in scenarios where the set of valid values might change frequently.&lt;&#x2F;p&gt;
&lt;p&gt;A CHECK constraint can be applied to a column to enforce that its value must be one of a predefined set of values. Here’s how you can define a table using a CHECK constraint as an alternative to an ENUM:&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; 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;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 style=&quot;color: #F97583;&quot;&gt;    status TEXT 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;    order_date &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 style=&quot;color: #F97583;&quot;&gt;    CHECK&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;status IN&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;new&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &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;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;processing&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;shipped&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &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;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;Modification of the constraints is possible, but you will always have to &lt;code&gt;DROP CONSTRAINT&lt;&#x2F;code&gt; and then &lt;code&gt;ADD CONSTRAINT&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 TABLE&lt;&#x2F;span&gt;&lt;span&gt; orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DROP CONSTRAINT&lt;&#x2F;span&gt;&lt;span&gt; orders_status_check;&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;ALTER TABLE&lt;&#x2F;span&gt;&lt;span&gt; orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ADD CONSTRAINT&lt;&#x2F;span&gt;&lt;span&gt; orders_status_check &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CHECK&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;status IN&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;new&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &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;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;processing&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;shipped&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &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;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;returned&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;While this operation is not as &#x27;heavyweight&#x27; as changing the data type, please be aware that an &lt;strong&gt;ACCESS EXCLUSIVE&lt;&#x2F;strong&gt; lock will be acquired for the entire table—meaning no other operations can be performed on the table. With a growing table size and increasing concurrency, this might still lead to application-level downtime.&lt;&#x2F;p&gt;
&lt;p&gt;Another limitation is the lack of direct support for sorting, meaning you will have to implement it manually in every query or abstract the sorting detail using a view.&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; id, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;status&lt;&#x2F;span&gt;&lt;span&gt;, order_date&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; orders&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ORDER BY&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ARRAY_POSITION(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        ARRAY&lt;&#x2F;span&gt;&lt;span&gt;[&amp;#39;new&amp;#39;, &amp;#39;pending&amp;#39;, &amp;#39;processing&amp;#39;, &amp;#39;shipped&amp;#39;, &amp;#39;delivered&amp;#39;, &amp;#39;returned&amp;#39;],&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&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;h2 id=&quot;reference-tables-as-the-real-alternative&quot;&gt;Reference Tables as the Real Alternative&lt;a class=&quot;zola-anchor&quot; href=&quot;#reference-tables-as-the-real-alternative&quot; aria-label=&quot;Anchor link for: reference-tables-as-the-real-alternative&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;It might be surprising, but one of the motivations for using either ENUMs or CHECK constraints is to avoid additional JOINs. It&#x27;s no surprise that JOINs are a source of pain for many people who struggle with SQL, and therefore they try to limit their use. While there might be negligible impact on performing such an operation, there are no other reasons not to use reference tables—it’s not storage (unless you over-optimise with &lt;code&gt;smallint&lt;&#x2F;code&gt; and extremely large datasets) and definitely not operational considerations.&lt;&#x2F;p&gt;
&lt;p&gt;In fact, reference tables are often better in most cases. They not only address all the use cases supported by both ENUMs and CHECK constraints, but they can also resolve the limitations of these methods.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;creating-and-using-a-reference-table&quot;&gt;Creating and Using a Reference Table&lt;a class=&quot;zola-anchor&quot; href=&quot;#creating-and-using-a-reference-table&quot; aria-label=&quot;Anchor link for: creating-and-using-a-reference-table&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;Let’s create a reference table similar to the ENUM data type we’ve discussed above:&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; order_statuses&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;    status_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT 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;    status_name &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT NOT NULL&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;CREATE UNIQUE INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; unique_status_name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; order_statuses (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;LOWER&lt;&#x2F;span&gt;&lt;span&gt;(status_name));&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; order_statuses (status_name) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&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: #9ECBFF;&quot;&gt;&amp;#39;new&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;(&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;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;processing&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;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;shipped&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;(&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;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;cancelled&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;h3 id=&quot;updating-the-orders-table-to-use-the-reference-table&quot;&gt;Updating the Orders Table to Use the Reference Table&lt;a class=&quot;zola-anchor&quot; href=&quot;#updating-the-orders-table-to-use-the-reference-table&quot; aria-label=&quot;Anchor link for: updating-the-orders-table-to-use-the-reference-table&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;To use the reference table, you’ll need to alter your existing &lt;code&gt;orders&lt;&#x2F;code&gt; table. This involves dropping the current &lt;code&gt;status&lt;&#x2F;code&gt; column if it’s using an ENUM or CHECK constraint, and replacing it with a foreign key that references the &lt;code&gt;order_statuses&lt;&#x2F;code&gt; table:&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 TABLE&lt;&#x2F;span&gt;&lt;span&gt; orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DROP&lt;&#x2F;span&gt;&lt;span&gt; COLUMN &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IF EXISTS 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;&#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; orders &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ADD&lt;&#x2F;span&gt;&lt;span&gt; COLUMN status_id &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 style=&quot;color: #F97583;&quot;&gt;ADD CONSTRAINT&lt;&#x2F;span&gt;&lt;span&gt; fk_order_status&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FOREIGN KEY&lt;&#x2F;span&gt;&lt;span&gt; (status_id) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;REFERENCES&lt;&#x2F;span&gt;&lt;span&gt; order_statuses(status_id);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;impact-of-using-reference-tables&quot;&gt;Impact of Using Reference Tables&lt;a class=&quot;zola-anchor&quot; href=&quot;#impact-of-using-reference-tables&quot; aria-label=&quot;Anchor link for: impact-of-using-reference-tables&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;While reference tables do not provide anything for free, such as ENUM ordering or simple CHECK constraints, they offer far greater flexibility. Here are some examples of what you can achieve with reference tables:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Custom Sorting Logic:&lt;&#x2F;strong&gt; You can implement specific sorting by adding an additional &lt;code&gt;sort_order&lt;&#x2F;code&gt; column to the &lt;code&gt;order_statuses&lt;&#x2F;code&gt; table. This allows you to easily change the order of statuses without altering the schema.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Deprecating Values:&lt;&#x2F;strong&gt; By adding a &lt;code&gt;deleted_at&lt;&#x2F;code&gt; column (or a similar field), you can deprecate certain status values. Combined with a trigger, you can prevent these deprecated values from being used in new data while maintaining historical records.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Renaming Values:&lt;&#x2F;strong&gt; Unlike ENUMs, renaming a status is straightforward - simply update the value in the &lt;code&gt;order_statuses&lt;&#x2F;code&gt; table.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Internationalisation:&lt;&#x2F;strong&gt; If your application needs to support multiple languages, you can extend the &lt;code&gt;order_statuses&lt;&#x2F;code&gt; table with additional columns for different languages or even create a separate lookup table that stores translations.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Additional Metadata:&lt;&#x2F;strong&gt; Reference tables can store extra information about each status, such as descriptions, colours (for UI purposes), or associated icons, which can be particularly useful in applications with rich user interfaces.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Having mentioned all advantages of using Reference Tables, it&#x27;s also important to mention the fact it&#x27;s not straightforward to set the default values for such a references. Definitely not in obvious way which ENUMs provide. The real alternative is to create table&#x2F;column without default value and alter it specifically for given database using &lt;code&gt;ALTER COLUMN ... SET DEFAULT&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 TABLE&lt;&#x2F;span&gt;&lt;span&gt; orders&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&lt;&#x2F;span&gt;&lt;span&gt; COLUMN status_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET DEFAULT&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; status_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; order_statuses &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; status_name &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&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;performance-considerations-enums-check-constraints-and-reference-tables&quot;&gt;Performance Considerations: ENUMs, CHECK Constraints, and Reference Tables&lt;a class=&quot;zola-anchor&quot; href=&quot;#performance-considerations-enums-check-constraints-and-reference-tables&quot; aria-label=&quot;Anchor link for: performance-considerations-enums-check-constraints-and-reference-tables&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Thanks to their internal representation as OIDs, ENUMs offer predictable performance and indexing strategies, often outperforming text-based CHECK constraints. Since ENUM comparisons rely on integers, they tend to be faster than string comparisons required for CHECK constraints. Reference tables, on the other hand, inherently require a JOIN, which introduces the overhead of accessing two tables and potentially working with multiple indexes.&lt;&#x2F;p&gt;
&lt;p&gt;While I initially planned to publish benchmarks comparing the performance of all three approaches, I decided not to include specific numbers. It’s true that ENUMs consistently performed the fastest, and reference tables were somewhat slower due to the necessary JOIN. However, as with most benchmarks, the scenarios were artificial. The actual performance difference, though measurable, is unlikely to be significant in practical applications. In any reasonably complex query, other factors are far more likely to impact performance than whether you use ENUMs or not.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;conclusion-use-enums-where-they-make-sense&quot;&gt;Conclusion: Use ENUMs Where They Make Sense&lt;a class=&quot;zola-anchor&quot; href=&quot;#conclusion-use-enums-where-they-make-sense&quot; aria-label=&quot;Anchor link for: conclusion-use-enums-where-they-make-sense&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;As with many decisions in software development, the use of ENUMs comes down to &quot;it depends.&quot; This article has provided clarity on how ENUMs work, where their limitations lie, and how to make an informed decision about when to use them.&lt;&#x2F;p&gt;
&lt;p&gt;To recap:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ENUMs&lt;&#x2F;strong&gt; are ideal for small, static sets of values that require strict type enforcement and minimal changes, preferably data types that are not directly tied to the business logic.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;CHECK constraints&lt;&#x2F;strong&gt; offer more flexibility than ENUMs, making them a good choice when the allowed values might change, but the data type remains simple, and there&#x27;s no&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Reference tables&lt;&#x2F;strong&gt; provide the most flexibility and scalability, making them the go-to choice for dynamic or complex value sets and when you want to avoid the limitations of ENUMs and CHECK constraints.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;But in the end, no plan ever survives first contact with reality. It’s essential to revisit and reconsider your decisions as your application evolves, ensuring that your database schema continues to serve the needs of your project as effectively as possible.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;em&gt;And while it&#x27;s not meant as advice - personally I&#x27;m still going to gravitate towards the advice &quot;don&#x27;t use ENUMs&quot; though. I&#x27;m yet to see value-based field that does not change over time.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Beyond Simple Upserts with MERGE in PostgreSQL</title>
        <published>2024-08-25T00:00:00+00:00</published>
        <updated>2024-08-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/beyond-upserts-with-merge/"/>
        <id>https://boringsql.com/posts/beyond-upserts-with-merge/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/beyond-upserts-with-merge/">&lt;p&gt;Understanding how comfortable someone is with databases and SQL often comes down to the features they use. In PostgreSQL, one such feature that distinguishes more advanced users is the &lt;code&gt;MERGE&lt;&#x2F;code&gt; command, introduced in version 15 and expanded in version 17 (in beta at the time of writing this article). Before &lt;code&gt;MERGE&lt;&#x2F;code&gt;, developers typically relied on &lt;code&gt;INSERT ... ON CONFLICT DO UPDATE&lt;&#x2F;code&gt; for upserts—a method introduced in PostgreSQL 9.5 that has since become a staple in many developers&#x27; toolkits.&lt;&#x2F;p&gt;
&lt;p&gt;While &lt;code&gt;ON CONFLICT&lt;&#x2F;code&gt; offers a straightforward solution for simple upsert scenarios, it can quickly become limiting as business logic grows in complexity. This is where the &lt;code&gt;MERGE&lt;&#x2F;code&gt; command excels. Introduced in the SQL:2003 standard, &lt;code&gt;MERGE&lt;&#x2F;code&gt; allows for more sophisticated data synchronisation tasks by combining multiple operations—such as conditional inserts, updates, and deletes—into a single, atomic statement.&lt;&#x2F;p&gt;
&lt;p&gt;In this article, we’ll explore the capabilities of the &lt;code&gt;MERGE&lt;&#x2F;code&gt; command, comparing it with traditional upsert methods and examining how it can streamline database operations. Through practical examples, we&#x27;ll illustrate how &lt;code&gt;MERGE&lt;&#x2F;code&gt; can simplify even the most complex workflows, making it an indispensable tool for developers working with PostgreSQL.&lt;&#x2F;p&gt;
&lt;p&gt;This introduction sets the stage by highlighting the evolution from &lt;code&gt;ON CONFLICT&lt;&#x2F;code&gt; to &lt;code&gt;MERGE&lt;&#x2F;code&gt;, explains the context in which &lt;code&gt;MERGE&lt;&#x2F;code&gt; becomes valuable, and clearly outlines what the reader will gain from the article.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hands-on-example-managing-a-score-points-system-for-a-mobile-game&quot;&gt;Hands-on Example: Managing a Score Points System for a Mobile Game&lt;a class=&quot;zola-anchor&quot; href=&quot;#hands-on-example-managing-a-score-points-system-for-a-mobile-game&quot; aria-label=&quot;Anchor link for: hands-on-example-managing-a-score-points-system-for-a-mobile-game&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Imagine you’re managing a tournament score system for an online game, where players earn special status by participating in weekly tournaments. The simple business logic would be:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Players can earn score points by participating in the tournament.&lt;&#x2F;li&gt;
&lt;li&gt;Players must participate in all tournaments to maintain their score (i.e., maintain the streak).&lt;&#x2F;li&gt;
&lt;li&gt;Players who participate in a second consecutive tournament can achieve the status of &lt;code&gt;veteran&lt;&#x2F;code&gt; (if they reach 1,000 score points) or gain the status of &lt;code&gt;star&lt;&#x2F;code&gt; (if they reach 100 or more score points).&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The score points are updated weekly, based on the outcome of the previous week&#x27;s participation.&lt;&#x2F;p&gt;
&lt;p&gt;We will start with a simple database schema consisting of a table &lt;code&gt;tournament_scores&lt;&#x2F;code&gt; that tracks (as the name suggests) only active users.&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; tournament_scores&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;    player_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT 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;    player_name &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT UNIQUE 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;    score &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT NOT NULL DEFAULT&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;&#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;newbie&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; CHECK&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;status IN&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;newbie&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;veteran&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;star&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;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And populate it with some sample 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;INSERT INTO&lt;&#x2F;span&gt;&lt;span&gt; tournament_scores (player_name, score, &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 style=&quot;color: #F97583;&quot;&gt;VALUES&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: #9ECBFF;&quot;&gt;&amp;#39;PlayerOne&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;900&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;newbie&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;),   &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Regular player, close to Veteran promotion&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: #9ECBFF;&quot;&gt;&amp;#39;PlayerTwo&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1200&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;veteran&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;), &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Already a Veteran player&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: #9ECBFF;&quot;&gt;&amp;#39;PlayerThree&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;300&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;newbie&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;); &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Regular player with a lower score&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;upsert-using-on-conflict&quot;&gt;Upsert using &lt;code&gt;ON CONFLICT&lt;&#x2F;code&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#upsert-using-on-conflict&quot; aria-label=&quot;Anchor link for: upsert-using-on-conflict&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;When activity data is received, there are multiple ways to process it. &lt;code&gt;ON CONFLICT&lt;&#x2F;code&gt; is used here for demonstration purposes.&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;INSERT INTO&lt;&#x2F;span&gt;&lt;span&gt; tournament_scores (player_name, score)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&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: #9ECBFF;&quot;&gt;&amp;#39;PlayerOne&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;50&lt;&#x2F;span&gt;&lt;span&gt;),    &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Add points for PlayerOne&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: #9ECBFF;&quot;&gt;&amp;#39;PlayerTwo&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;120&lt;&#x2F;span&gt;&lt;span&gt;),   &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Add points for PlayerTwo&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: #9ECBFF;&quot;&gt;&amp;#39;PlayerFour&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;70&lt;&#x2F;span&gt;&lt;span&gt;)    &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- New player without an account, PlayerFour&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span&gt; CONFLICT (player_name)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;DO &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;UPDATE SET&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    score &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; tournament_scores&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; EXCLUDED&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score&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 = CASE WHEN&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;tournament_scores&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; EXCLUDED&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score&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; 1000&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; THEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;veteran&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ELSE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;newbie&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; END&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 performs the basic requirements, updating the scores and possibly evaluating the status for existing tournament users. To remove users who no longer participated (and hence broke the streak) and apply other conditional logic, you would need to use separate statements.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;handling-upserts-with-merge&quot;&gt;Handling Upserts with &lt;code&gt;MERGE&lt;&#x2F;code&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#handling-upserts-with-merge&quot; aria-label=&quot;Anchor link for: handling-upserts-with-merge&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Now, let’s introduce the &lt;code&gt;MERGE&lt;&#x2F;code&gt; command and implement the same logic as above.&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;MERGE INTO&lt;&#x2F;span&gt;&lt;span&gt; tournament_scores ts&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;USING&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;    VALUES&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: #9ECBFF;&quot;&gt;&amp;#39;PlayerOne&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;50&lt;&#x2F;span&gt;&lt;span&gt;),    &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Add points for PlayerOne&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: #9ECBFF;&quot;&gt;&amp;#39;PlayerTwo&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;120&lt;&#x2F;span&gt;&lt;span&gt;),   &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Add points for PlayerTwo&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: #9ECBFF;&quot;&gt;&amp;#39;PlayerFour&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;70&lt;&#x2F;span&gt;&lt;span&gt;)    &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- New player without an account, PlayerFour&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;AS&lt;&#x2F;span&gt;&lt;span&gt; v(player_name, score_added)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; ts&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;player_name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; v&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;player_name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHEN MATCHED THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    UPDATE SET&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      score &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; ts&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; v&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score_added&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 = CASE WHEN&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;ts&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; v&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score_added&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; 1000&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; THEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;veteran&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ELSE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;newbie&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; END&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHEN NOT MATCHED THEN&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&lt;&#x2F;span&gt;&lt;span&gt; (player_name, score)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;v&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;player_name&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;v&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score_added&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;Here, we define the table &lt;code&gt;tournament_scores&lt;&#x2F;code&gt; as the &lt;strong&gt;target&lt;&#x2F;strong&gt; and the list of VALUES as the &lt;strong&gt;source&lt;&#x2F;strong&gt;. Using a conditional clause, we define the logic for both matched and unmatched entries, giving us both INSERT and UPDATE paths.&lt;&#x2F;p&gt;
&lt;p&gt;The evaluation paths in this case are called &lt;code&gt;when_clauses&lt;&#x2F;code&gt;. The &lt;code&gt;MERGE&lt;&#x2F;code&gt; statement allows you to specify multiple clauses with different conditions, for example, allowing you to award users &lt;code&gt;star&lt;&#x2F;code&gt; status if they gain more than 100 score points within a given tournament.&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;MERGE INTO&lt;&#x2F;span&gt;&lt;span&gt; tournament_scores ts&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;USING&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;    VALUES&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: #9ECBFF;&quot;&gt;&amp;#39;PlayerOne&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;50&lt;&#x2F;span&gt;&lt;span&gt;),    &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Add points for PlayerOne&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: #9ECBFF;&quot;&gt;&amp;#39;PlayerTwo&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;120&lt;&#x2F;span&gt;&lt;span&gt;),   &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Add points for PlayerTwo&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: #9ECBFF;&quot;&gt;&amp;#39;PlayerFour&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;70&lt;&#x2F;span&gt;&lt;span&gt;)    &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- New player without an account, PlayerFour&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;AS&lt;&#x2F;span&gt;&lt;span&gt; v(player_name, score_added)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; ts&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;player_name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; v&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;player_name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHEN MATCHED AND&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; v&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score_added&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; 100&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    UPDATE SET&lt;&#x2F;span&gt;&lt;span&gt; score &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; ts&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; v&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score_added&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;status =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;star&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHEN MATCHED THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    UPDATE SET&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      score &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; ts&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; v&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score_added&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 = CASE WHEN&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;ts&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; v&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score_added&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; 1000&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; THEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;veteran&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ELSE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;newbie&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; END&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHEN NOT MATCHED THEN&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&lt;&#x2F;span&gt;&lt;span&gt; (player_name, score)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;v&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;player_name&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;v&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score_added&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;Here’s a summary of how it works:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;MERGE&lt;&#x2F;code&gt; command evaluates the &lt;code&gt;when_clauses&lt;&#x2F;code&gt; in the order they are written.&lt;&#x2F;li&gt;
&lt;li&gt;If a row matches the condition specified in a clause, the action defined in that clause is performed, and the row is no longer eligible to be matched against subsequent &lt;code&gt;when_clauses&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;While we introduced multiple evaluation paths, the &lt;code&gt;MERGE&lt;&#x2F;code&gt; command in PostgreSQL goes beyond that and expands the &lt;code&gt;WHEN NOT MATCHED&lt;&#x2F;code&gt; clause, which effectively becomes &lt;code&gt;WHEN NOT MATCHED [BY TARGET]&lt;&#x2F;code&gt;. PostgreSQL allows you to specify &lt;code&gt;WHEN NOT MATCHED BY SOURCE&lt;&#x2F;code&gt; to perform the necessary merge statement for the data not present in the source table.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;handling-deletes-with-merge&quot;&gt;Handling DELETEs with &lt;code&gt;MERGE&lt;&#x2F;code&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#handling-deletes-with-merge&quot; aria-label=&quot;Anchor link for: handling-deletes-with-merge&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The functionality completely missing from regular upserts with &lt;code&gt;ON CONFLICT&lt;&#x2F;code&gt; is the ability to delete missing entries. In our sample scenario, we want to penalise players who broke their streak and did not participate in last week&#x27;s tournament by effectively deleting their entries from the target table.&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;MERGE INTO&lt;&#x2F;span&gt;&lt;span&gt; tournament_scores ts&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;USING&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;    VALUES&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: #9ECBFF;&quot;&gt;&amp;#39;PlayerOne&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;50&lt;&#x2F;span&gt;&lt;span&gt;),    &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Add points for PlayerOne&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: #9ECBFF;&quot;&gt;&amp;#39;PlayerTwo&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;120&lt;&#x2F;span&gt;&lt;span&gt;),   &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Add points for PlayerTwo&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: #9ECBFF;&quot;&gt;&amp;#39;PlayerFour&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;70&lt;&#x2F;span&gt;&lt;span&gt;)    &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- New player without an account, PlayerFour&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;AS&lt;&#x2F;span&gt;&lt;span&gt; v(player_name, score_added)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; ts&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;player_name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; v&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;player_name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHEN MATCHED AND&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; v&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score_added&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; 100&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    UPDATE SET&lt;&#x2F;span&gt;&lt;span&gt; score &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; ts&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; v&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score_added&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;status =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;star&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHEN MATCHED THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    UPDATE SET&lt;&#x2F;span&gt;&lt;span&gt; score &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; ts&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; v&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score_added&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHEN NOT MATCHED THEN&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&lt;&#x2F;span&gt;&lt;span&gt; (player_name, score)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;v&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;player_name&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;v&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;score_added&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;WHEN NOT MATCHED BY&lt;&#x2F;span&gt;&lt;span&gt; SOURCE &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    DELETE&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;The introduction of the &lt;code&gt;DELETE&lt;&#x2F;code&gt; merge operation complements all possible &lt;code&gt;MERGE&lt;&#x2F;code&gt; outcomes:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;The first is &lt;code&gt;merge_update&lt;&#x2F;code&gt;, applicable to &lt;code&gt;WHEN MATCHED&lt;&#x2F;code&gt; and &lt;code&gt;WHEN NOT MATCHED&lt;&#x2F;code&gt; clauses, consisting of regular &lt;code&gt;UPDATE&lt;&#x2F;code&gt; operations.&lt;&#x2F;li&gt;
&lt;li&gt;The second is &lt;code&gt;merge_insert&lt;&#x2F;code&gt; for the &lt;code&gt;WHEN NOT MATCHED&lt;&#x2F;code&gt; clause, allowing (as the name implies) data to be inserted.&lt;&#x2F;li&gt;
&lt;li&gt;The &lt;code&gt;DELETE&lt;&#x2F;code&gt; we introduce is part of &lt;code&gt;merge_delete&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Technically, there&#x27;s also the ability to specify &lt;code&gt;DO NOTHING&lt;&#x2F;code&gt; for all available &lt;code&gt;when_clauses&lt;&#x2F;code&gt;, similar to upserts using &lt;code&gt;ON CONFLICT&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;using-merge-output&quot;&gt;Using &lt;code&gt;MERGE&lt;&#x2F;code&gt; Output&lt;a class=&quot;zola-anchor&quot; href=&quot;#using-merge-output&quot; aria-label=&quot;Anchor link for: using-merge-output&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Starting from PostgreSQL 17, the &lt;code&gt;MERGE&lt;&#x2F;code&gt; command has been extended to support &lt;code&gt;RETURNING&lt;&#x2F;code&gt; clauses to process merged data further. When an &lt;code&gt;INSERT&lt;&#x2F;code&gt; or &lt;code&gt;UPDATE&lt;&#x2F;code&gt; action is performed, the new values of the target table&#x27;s columns are used. When a &lt;code&gt;DELETE&lt;&#x2F;code&gt; is performed, the old values of the target table&#x27;s columns are used.&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;The &lt;code&gt;MERGE&lt;&#x2F;code&gt; command significantly enhances the ability to handle complex &lt;code&gt;INSERT&lt;&#x2F;code&gt; and &lt;code&gt;UPDATE&lt;&#x2F;code&gt; logic by allowing multiple operations within a single query while also capturing even the most intricate scenarios. In this article, we have explored the syntax and common use cases of &lt;code&gt;MERGE&lt;&#x2F;code&gt;. For further exploration, note that the &lt;code&gt;source&lt;&#x2F;code&gt; dataset is where you can perform any data transformations required, making &lt;code&gt;MERGE&lt;&#x2F;code&gt; particularly well-suited for ETL operations, data archival, cleanup tasks, and more.&lt;&#x2F;p&gt;
&lt;p&gt;Incorporating &lt;code&gt;MERGE&lt;&#x2F;code&gt; into your PostgreSQL toolkit can simplify database operations, reduce the risk of data inconsistencies, and streamline your&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Gentle Introduction to Window Functions in PostgreSQL</title>
        <published>2024-07-07T00:00:00+00:00</published>
        <updated>2024-07-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/window-functions-introduction/"/>
        <id>https://boringsql.com/posts/window-functions-introduction/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/window-functions-introduction/">&lt;p&gt;Understanding the relationship between data points is crucial. For instance, you might need to identify the most recent orders for each customer or track changes in sensor readings over time. Unlike aggregate functions, which summarise data into a single row, it is window functions that allow you to analyse data while preserving each row’s details. This is the core of the logic, but don’t worry if you struggle to imagine the difference, as we will cover all of it in this article.&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL supports SQL window functions, facilitating complex calculations across related rows within a table. These functions are particularly useful for tasks such as ranking entries, calculating running totals, finding moving averages, and comparing individual entries. Mastering window functions can significantly enhance your data analysis capabilities.&lt;&#x2F;p&gt;
&lt;p&gt;You can easily use similar window functions as you would with aggregation. Let’s take a simple example of sensor readings:&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; sensor_readings&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;    sensor_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;bigint&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;    reading_value &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;decimal&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;    reading_time &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamp with time zone default&lt;&#x2F;span&gt;&lt;span&gt; current_timestamp&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; sensor_readings (sensor_id, reading_value, reading_time) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&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;32&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;7&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;2024-07-01 11:24:34&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;(&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;33&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: #9ECBFF;&quot;&gt;&amp;#39;2024-07-02 11:29:01&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;(&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;33&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: #9ECBFF;&quot;&gt;&amp;#39;2024-07-02 12:03:59&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;(&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;33&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: #9ECBFF;&quot;&gt;&amp;#39;2024-07-03 10:12:15&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;(&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: #79B8FF;&quot;&gt;32&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;8&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;2024-07-01 13:17:01&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;(&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: #79B8FF;&quot;&gt;35&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;8&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;2024-07-02 09:18:11&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;(&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;span style=&quot;color: #79B8FF;&quot;&gt;29&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: #9ECBFF;&quot;&gt;&amp;#39;2024-07-01 13:54:03&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;(&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;span style=&quot;color: #79B8FF;&quot;&gt;30&lt;&#x2F;span&gt;&lt;span&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;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;2024-07-01 14:12:09&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;(&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;span style=&quot;color: #79B8FF;&quot;&gt;31&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: #9ECBFF;&quot;&gt;&amp;#39;2024-07-02 16:07:43&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;When you start with the aggregation functions, it is straightforward for anybody familiar with SQL:&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    sensor_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    AVG&lt;&#x2F;span&gt;&lt;span&gt;(reading_value)&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; sensor_readings&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GROUP BY&lt;&#x2F;span&gt;&lt;span&gt; sensor_id;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;with the output&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; sensor_id |         avg&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;         3 | 30.3000000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         2 | 34.3000000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         1 | 32.9750000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;While similar, using the window function AVG we can get almost same 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 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;    sensor_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    reading_value,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    AVG&lt;&#x2F;span&gt;&lt;span&gt;(reading_value) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;OVER&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;PARTITION BY&lt;&#x2F;span&gt;&lt;span&gt; sensor_id) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; avg_reading_value&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; sensor_readings;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;such as&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; sensor_id | reading_value |  avg_reading_value&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;         1 |          32.7 | 32.9750000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         1 |          33.1 | 32.9750000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         1 |          33.1 | 32.9750000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         1 |          33.0 | 32.9750000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         2 |          32.8 | 34.3000000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         2 |          35.8 | 34.3000000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         3 |          29.1 | 30.3000000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         3 |          30.3 | 30.3000000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         3 |          31.5 | 30.3000000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This reiterates the fundamental difference between the two sets of functions. As mentioned earlier, while the results for &lt;code&gt;sensor_id&lt;&#x2F;code&gt; are the same in both cases, the &lt;strong&gt;aggregate function&lt;&#x2F;strong&gt; summarised it into a single row (grouped by &lt;code&gt;sensor_id&lt;&#x2F;code&gt;), whereas the &lt;strong&gt;window function&lt;&#x2F;strong&gt; provides the value for the set of rows in the partition defined by &lt;code&gt;sensor_id&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;After showing the average in a window function, it’s important to note that traditional aggregation functions might not be the most helpful in the context of window function logic. Despite functions like &lt;code&gt;AVG&lt;&#x2F;code&gt;, &lt;code&gt;COUNT&lt;&#x2F;code&gt;, and &lt;code&gt;SUM&lt;&#x2F;code&gt;—which are the most used aggregation functions—being available as window functions, we used the above only to demonstrate the difference.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;over-clause&quot;&gt;OVER clause&lt;a class=&quot;zola-anchor&quot; href=&quot;#over-clause&quot; aria-label=&quot;Anchor link for: over-clause&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Before diving into the individual functions, let’s cover the syntax first. From the sample query above, you already get the basic syntax of the window functions, with the &lt;code&gt;OVER&lt;&#x2F;code&gt; clause being a primary identification of windowing functionality.&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;window_function ([expression...]) OVER window_definition&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In our example, the basic window functions can be similar to the aggregate ones— like &lt;code&gt;AVG&lt;&#x2F;code&gt;, &lt;code&gt;SUM&lt;&#x2F;code&gt; and &lt;code&gt;COUNT&lt;&#x2F;code&gt; - and a number of the functions we will cover shortly.&lt;&#x2F;p&gt;
&lt;p&gt;The window definition part specifies how the window function will see the data it works over. It can include:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Partitioning&lt;&#x2F;strong&gt; to divide the result sets into separate partitions (think groups in aggregate functions).&lt;&#x2F;li&gt;
&lt;li&gt;Definition of the &lt;strong&gt;ordering&lt;&#x2F;strong&gt; of the rows within each partition.&lt;&#x2F;li&gt;
&lt;li&gt;Specification of how to apply &lt;strong&gt;framing&lt;&#x2F;strong&gt; of the subset of rows for each row’s calculation.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;From all the components of the window syntax, only partitioning is mandatory.&lt;&#x2F;p&gt;
&lt;p&gt;If we revisit our first window function example above, you can identify the &lt;strong&gt;partitioning&lt;&#x2F;strong&gt; part (&lt;code&gt;PARTITION BY sensor_id&lt;&#x2F;code&gt;). As already mentioned, you can easily compare the partitioning logic to the grouping used in aggregate functions, with the same properties—like partitioning data segments by multiple fields, using functions and other logic.&lt;&#x2F;p&gt;
&lt;p&gt;With partitioning comes hand in hand &lt;strong&gt;ordering&lt;&#x2F;strong&gt; of the data. When you start thinking of row properties, instead of grouping data into a single row, order starts to make a difference. Let’s take the following 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&gt;window_function ([expression...]) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;OVER&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;PARTITION BY&lt;&#x2F;span&gt;&lt;span&gt; sensor_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ORDER BY&lt;&#x2F;span&gt;&lt;span&gt; reading_time)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This will provide different data compared to unsorted ones. If you struggle to find the application for this, think of the moving average or row numbering.&lt;&#x2F;p&gt;
&lt;p&gt;The last component of the window definition is &lt;strong&gt;framing&lt;&#x2F;strong&gt;, allowing you to further limit the data over which the window function is calculated. There are two framing expressions: &lt;code&gt;ROWS BETWEEN&lt;&#x2F;code&gt; and &lt;code&gt;RANGE BETWEEN&lt;&#x2F;code&gt;. Using the framing, we can turn AVG from the above example to provide our first real use case when you might want to use window functions.&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    reading_time,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    sensor_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    reading_value,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    AVG&lt;&#x2F;span&gt;&lt;span&gt;(reading_value) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;OVER&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;        PARTITION BY&lt;&#x2F;span&gt;&lt;span&gt; sensor_id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        ORDER BY&lt;&#x2F;span&gt;&lt;span&gt; reading_time&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        ROWS BETWEEN&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; PRECEDING AND&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; FOLLOWING&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;AS&lt;&#x2F;span&gt;&lt;span&gt; moving_avg_reading_value&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; sensor_readings;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Implementing the moving average of the individual readings, taking into account a maximum of 3 rows including the current one.&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;      reading_time      | sensor_id | reading_value | moving_avg_reading_value&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; 2024-07-01 11:24:34+02 |         1 |          32.7 |      32.9000000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 2024-07-02 11:29:01+02 |         1 |          33.1 |      32.9666666666666667&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 2024-07-02 12:03:59+02 |         1 |          33.1 |      33.0666666666666667&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 2024-07-03 10:12:15+02 |         1 |          33.0 |      33.0500000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 2024-07-01 13:17:01+02 |         2 |          32.8 |      34.3000000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 2024-07-02 09:18:11+02 |         2 |          35.8 |      34.3000000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 2024-07-01 13:54:03+02 |         3 |          29.1 |      29.7000000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 2024-07-01 14:12:09+02 |         3 |          30.3 |      30.3000000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; 2024-07-02 16:07:43+02 |         3 |          31.5 |      30.9000000000000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;exploring-window-functions&quot;&gt;Exploring Window functions&lt;a class=&quot;zola-anchor&quot; href=&quot;#exploring-window-functions&quot; aria-label=&quot;Anchor link for: exploring-window-functions&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Now that we have a foundational understanding of window functions and their components, let’s dive into specific window functions. We’ll cover some of the most commonly used functions, such as &lt;code&gt;ROW_NUMBER()&lt;&#x2F;code&gt;, &lt;code&gt;RANK()&lt;&#x2F;code&gt;, &lt;code&gt;DENSE_RANK()&lt;&#x2F;code&gt;, &lt;code&gt;LAG()&lt;&#x2F;code&gt;, &lt;code&gt;LEAD()&lt;&#x2F;code&gt;, &lt;code&gt;FIRST_VALUE()&lt;&#x2F;code&gt;, &lt;code&gt;LAST_VALUE()&lt;&#x2F;code&gt; and more. For each function, we’ll provide examples to illustrate their practical applications.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;row-number&quot;&gt;ROW_NUMBER&lt;a class=&quot;zola-anchor&quot; href=&quot;#row-number&quot; aria-label=&quot;Anchor link for: row-number&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;The &lt;code&gt;ROW_NUMBER()&lt;&#x2F;code&gt; function assigns a unique sequential integer to rows within a partition of a result set, starting with one for the first row in each partition.&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    sensor_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    reading_time,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    reading_value,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    ROW_NUMBER&lt;&#x2F;span&gt;&lt;span&gt;() &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;OVER&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;PARTITION BY&lt;&#x2F;span&gt;&lt;span&gt; sensor_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ORDER BY&lt;&#x2F;span&gt;&lt;span&gt; reading_time) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; row_num&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; sensor_readings;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This makes it easy to find the first&#x2F;last entries for a specified window. As an example, you can experiment with getting only the last reading for each hour.&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    sensor_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    reading_time,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    reading_value&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; (&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;        sensor_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        reading_time,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        reading_value,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        ROW_NUMBER&lt;&#x2F;span&gt;&lt;span&gt;() &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;OVER&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;            PARTITION BY&lt;&#x2F;span&gt;&lt;span&gt; sensor_id, date_trunc(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;hour&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, reading_time)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;            ORDER BY&lt;&#x2F;span&gt;&lt;span&gt; reading_time &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DESC&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;AS&lt;&#x2F;span&gt;&lt;span&gt; row_num&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; sensor_readings&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;AS&lt;&#x2F;span&gt;&lt;span&gt; ranked_readings&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; row_num &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;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;rank-and-dense-rank&quot;&gt;RANK and DENSE_RANK&lt;a class=&quot;zola-anchor&quot; href=&quot;#rank-and-dense-rank&quot; aria-label=&quot;Anchor link for: rank-and-dense-rank&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;The &lt;code&gt;RANK()&lt;&#x2F;code&gt; and &lt;code&gt;DENSE_RANK()&lt;&#x2F;code&gt; functions are used to assign a rank to each row within a partition, based on the order of one or more values. These functions are useful when scoring the values and handling ties. The main difference between &lt;code&gt;RANK&lt;&#x2F;code&gt; and &lt;code&gt;DENSE_RANK&lt;&#x2F;code&gt; is how they deal with gaps.&lt;&#x2F;p&gt;
&lt;p&gt;Example use to find the highest reading_value per sensor:&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    sensor_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    reading_time,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    reading_value,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    RANK&lt;&#x2F;span&gt;&lt;span&gt;() &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;OVER&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;PARTITION BY&lt;&#x2F;span&gt;&lt;span&gt; sensor_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ORDER BY&lt;&#x2F;span&gt;&lt;span&gt; reading_value &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DESC&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; rank&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; sensor_readings;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If you consider the sorting for the &lt;code&gt;sensor_id&lt;&#x2F;code&gt; 1 in our sample seed the difference between &lt;code&gt;RANK&lt;&#x2F;code&gt; and &lt;code&gt;DENSE_RANK&lt;&#x2F;code&gt; is easy to demonstrate.&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; sensor_id |      reading_time      | reading_value | rank&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;         1 | 2024-07-02 11:29:01+02 |          33.1 |    1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         1 | 2024-07-02 12:03:59+02 |          33.1 |    1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         1 | 2024-07-03 10:12:15+02 |          33.0 |    3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         1 | 2024-07-01 11:24:34+02 |          32.7 |    4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Giving a natural ranking with tie on the reading value 33.1 and leaving a gap on 2nd rank, whereas &lt;code&gt;DENSE_RANK&lt;&#x2F;code&gt; wouldn&#x27;t include the gap.&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; sensor_id |      reading_time      | reading_value | rank&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;         1 | 2024-07-02 11:29:01+02 |          33.1 |    1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         1 | 2024-07-02 12:03:59+02 |          33.1 |    1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         1 | 2024-07-03 10:12:15+02 |          33.0 |    2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         1 | 2024-07-01 11:24:34+02 |          32.7 |    3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;lag-and-lead&quot;&gt;&lt;code&gt;LAG&lt;&#x2F;code&gt; and &lt;code&gt;LEAD&lt;&#x2F;code&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#lag-and-lead&quot; aria-label=&quot;Anchor link for: lag-and-lead&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;To evaluate previous or subsequent rows without the need to self-join the dataset, you can utilise the power of the window functions &lt;code&gt;LAG&lt;&#x2F;code&gt; and &lt;code&gt;LEAD&lt;&#x2F;code&gt;. These functions are particularly useful for calculating differences between rows, comparing current and previous values, or fetching future values for comparison.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;code&gt;LAG&lt;&#x2F;code&gt; function provides access to a value in a previous row within the partition, while &lt;code&gt;LEAD&lt;&#x2F;code&gt; provides access to a subsequent row.&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    sensor_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    reading_time,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    reading_value,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    LAG&lt;&#x2F;span&gt;&lt;span&gt;(reading_value, &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;OVER&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;PARTITION BY&lt;&#x2F;span&gt;&lt;span&gt; sensor_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ORDER BY&lt;&#x2F;span&gt;&lt;span&gt; reading_time) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; previous_value,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    LEAD&lt;&#x2F;span&gt;&lt;span&gt;(reading_value, &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;OVER&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;PARTITION BY&lt;&#x2F;span&gt;&lt;span&gt; sensor_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ORDER BY&lt;&#x2F;span&gt;&lt;span&gt; reading_time) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; next_value&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; sensor_readings;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In this query, we are using a value of 1, but you can choose any position necessary. By using &lt;code&gt;LAG()&lt;&#x2F;code&gt; and &lt;code&gt;LEAD()&lt;&#x2F;code&gt;, you can perform advanced analyses that require looking backward or forward within your dataset, making it easier to derive meaningful insights and trends.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;first-value-and-last-values&quot;&gt;&lt;code&gt;FIRST_value&lt;&#x2F;code&gt; and &lt;code&gt;LAST_VALUES&lt;&#x2F;code&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#first-value-and-last-values&quot; aria-label=&quot;Anchor link for: first-value-and-last-values&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;The functions &lt;code&gt;FIRST_value&lt;&#x2F;code&gt; and &lt;code&gt;LAST_VALUES&lt;&#x2F;code&gt; are similar, except they (as the name says) give the first&#x2F;last value of the partition.&lt;&#x2F;p&gt;
&lt;p&gt;The &lt;code&gt;FIRST_VALUE&lt;&#x2F;code&gt; is useful when comparing the partition values to the first value (for example opening price for a day), and &lt;code&gt;LAST_VALUE&lt;&#x2F;code&gt; to identity the closing values.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;other-window-functions&quot;&gt;Other Window functions&lt;a class=&quot;zola-anchor&quot; href=&quot;#other-window-functions&quot; aria-label=&quot;Anchor link for: other-window-functions&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;The complete list of the window functions is available in &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;functions-window.html&quot;&gt;the documentation&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;re-using-the-window-definition&quot;&gt;Re-using the window definition&lt;a class=&quot;zola-anchor&quot; href=&quot;#re-using-the-window-definition&quot; aria-label=&quot;Anchor link for: re-using-the-window-definition&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;p&gt;As you might have noticed, the query we used to demonstrate &lt;code&gt;LAG&lt;&#x2F;code&gt; and &lt;code&gt;LEAD&lt;&#x2F;code&gt; was rather verbose. This was due to the repeated definition of the window for both columns. Luckily, the syntax of the window functions allows you to define and re-use the window definition.&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    sensor_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    reading_time,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    reading_value,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    LAG&lt;&#x2F;span&gt;&lt;span&gt;(reading_value, &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;OVER&lt;&#x2F;span&gt;&lt;span&gt; readings_window &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; previous_value,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    LEAD&lt;&#x2F;span&gt;&lt;span&gt;(reading_value, &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;OVER&lt;&#x2F;span&gt;&lt;span&gt; readings_window &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; next_value&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; sensor_readings&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WINDOW&lt;&#x2F;span&gt;&lt;span&gt; readings_window &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;PARTITION BY&lt;&#x2F;span&gt;&lt;span&gt; sensor_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ORDER BY&lt;&#x2F;span&gt;&lt;span&gt; reading_time);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This way you can ensure the consistent window definition and consistency in complex queries.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The time keepers: pg_cron and pg_timetable</title>
        <published>2024-06-15T00:00:00+00:00</published>
        <updated>2024-06-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/time-keepers-pg-cron-pg-timetable/"/>
        <id>https://boringsql.com/posts/time-keepers-pg-cron-pg-timetable/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/time-keepers-pg-cron-pg-timetable/">&lt;p&gt;Working with PostgreSQL, and virtually any database system, extends far beyond merely inserting and retrieving data. Many application and business processes, maintenance tasks, reporting, and orchestration tasks require the integration of a job scheduler. While third-party tools can drive automation, you can also automate the execution of predefined tasks directly within the database environment. Although system-level cron might be a starting point, the power of the database system lies in its ability to store all the necessary information alongside your data&#x2F;schema. In this article, we will explore &lt;code&gt;pg_cron&lt;&#x2F;code&gt; and &lt;code&gt;pg_timetable&lt;&#x2F;code&gt; as two distinct PostgreSQL-specific tools for scheduled task automation.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-many-roles-of-job-scheduling&quot;&gt;The Many Roles of Job Scheduling&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-many-roles-of-job-scheduling&quot; aria-label=&quot;Anchor link for: the-many-roles-of-job-scheduling&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Usually, the first requirement to automate job execution is the optimisation of the PostgreSQL cluster and databases. Except in very low usage scenarios, &lt;strong&gt;routine maintenance&lt;&#x2F;strong&gt; is the foundation of all database deployments. Whether it is VACUUMing, index rebuilding, or updating statistics, these are tasks that you will eventually need to automate to maintain operational efficiency.&lt;&#x2F;p&gt;
&lt;p&gt;Job scheduling is also indispensable for &lt;strong&gt;maintaining data quality&lt;&#x2F;strong&gt;, particularly through operations like refreshing materialised views or batch removal of outdated data from the system. From there, it is just a step to &lt;strong&gt;reporting&lt;&#x2F;strong&gt;, which can involve the automation of generating operational and business reports.&lt;&#x2F;p&gt;
&lt;p&gt;I also believe that databases are an excellent place to &lt;strong&gt;coordinate business processes&lt;&#x2F;strong&gt;. Automating data flows between components, teams, and departments helps create agile systems. As mentioned above, there is no better place to store the description of such automations than alongside your data.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pg-cron-automation-s-first-gear&quot;&gt;pg_cron: Automation&#x27;s First Gear&lt;a class=&quot;zola-anchor&quot; href=&quot;#pg-cron-automation-s-first-gear&quot; aria-label=&quot;Anchor link for: pg-cron-automation-s-first-gear&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Traditionally, the role of automation started with operating system tools like cron or Task Scheduler. That&#x27;s where &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;citusdata&#x2F;pg_cron&quot;&gt;&lt;code&gt;pg_cron&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; comes in. It&#x27;s a PostgreSQL extension that provides the simplicity and familiarity of cron&#x27;s scheduling directly within the database environment.&lt;&#x2F;p&gt;
&lt;p&gt;Due to its dependency on a shared library (because of the use of the background worker), it requires a full cluster restart. Nevertheless, due to its popularity, it is available within most managed cloud environments.&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;# requires &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;configuration update in&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; postgresql&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;conf&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;or&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; conf&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;d&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;shared_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_cron&amp;#39;&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&gt;#&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; add&lt;&#x2F;span&gt;&lt;span&gt; extension &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;as&lt;&#x2F;span&gt;&lt;span&gt; superuser&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&lt;&#x2F;span&gt;&lt;span&gt; EXTENSION pg_cron;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;As long as you are familiar with cron-like syntax, you can get started immediately by using commands like:&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 style=&quot;color: #79B8FF;&quot;&gt; cron&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;schedule&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;30 3 * * 6&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, $$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DELETE FROM&lt;&#x2F;span&gt;&lt;span&gt; events &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; event_time &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;lt; now&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; interval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;1 week&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;By default, &lt;code&gt;pg_cron&lt;&#x2F;code&gt; exposes its functionality in the &lt;code&gt;postgres&lt;&#x2F;code&gt; database (configurable), where it expects its metadata tables. Personally, I consider this a drawback, as it makes it less obvious to the casual DBA who might not be aware of the scheduling logic present.&lt;&#x2F;p&gt;
&lt;p&gt;In the same database, you can perform basic monitoring, for example, getting details of running and recently completed jobs:&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 * FROM&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; cron&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;job_run_details&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ORDER BY&lt;&#x2F;span&gt;&lt;span&gt; start_time &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DESC LIMIT&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;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;There is no direct support to talk to other PostgreSQL clusters (you would have to facilitate this, for example, using Foreign Data Wrappers).&lt;&#x2F;p&gt;
&lt;p&gt;While cron-like syntax is beneficial for adoption, it is where &lt;code&gt;pg_cron&lt;&#x2F;code&gt; falls short, as it does not support advanced cases like task chaining, dependencies, and triggers.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;full-speed-with-pg-timetable&quot;&gt;Full Speed with pg_timetable&lt;a class=&quot;zola-anchor&quot; href=&quot;#full-speed-with-pg-timetable&quot; aria-label=&quot;Anchor link for: full-speed-with-pg-timetable&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;If &lt;code&gt;pg_cron&lt;&#x2F;code&gt; offers automation in first gear, &lt;code&gt;pg_timetable&lt;&#x2F;code&gt; is where you can go full speed ahead. It not only provides cron-like syntax but elevates it to a whole new level with task chaining, parameter support, multiple execution clients, enhanced scheduling, and much more.&lt;&#x2F;p&gt;
&lt;p&gt;The first difference you might notice is in its distribution. Timetable is not a PostgreSQL extension but a standalone binary (or available as a Docker image) that you have to configure and run. This immediately raises the bar in terms of the infrastructure it requires. On the other hand, not being distributed as an extension makes it compatible with all managed services by default (if you can get the process up and running).&lt;&#x2F;p&gt;
&lt;p&gt;While the basic syntax might be similar to &lt;code&gt;pg_cron&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 style=&quot;color: #79B8FF;&quot;&gt; timetable&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;add_job&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;execute-func&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;5 0 * 8 *&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;SELECT public.my_func()&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;this is not anywhere near the limits of &lt;code&gt;pg_timetable&lt;&#x2F;code&gt;. &lt;code&gt;add_job&lt;&#x2F;code&gt; is a helper function that creates a simple one-task chain. For task execution, it directly supports more options, including built-in tasks (like sending emails if properly configured, downloading files, sleeping, copying files, etc.), external commands, the ability to choose which client should execute the job, concurrency, and more.&lt;&#x2F;p&gt;
&lt;p&gt;In its full definition, &lt;code&gt;add_job&lt;&#x2F;code&gt; is quite powerful (see &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pg-timetable.readthedocs.io&#x2F;en&#x2F;master&#x2F;basic_jobs.html#add-simple-job&quot;&gt;documentation&lt;&#x2F;a&gt; for details):&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 style=&quot;color: #79B8FF;&quot;&gt; timetable&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;add_job&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;    job_name            &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;notify every minute&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;    job_schedule        &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;* * * * *&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;    job_command         &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;SELECT pg_notify($1, $2)&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;    job_parameters      &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;[&amp;quot;TT_CHANNEL&amp;quot;, &amp;quot;Hello World!&amp;quot;]&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::jsonb,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    job_kind            &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;SQL&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;timetable&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;command_kind&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;    job_client_name     &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&amp;gt; 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;    job_max_instances   &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; 1&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;    job_live            &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; TRUE,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    job_self_destruct   &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&amp;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;span&gt;    job_ignore_errors   &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; TRUE&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;AS&lt;&#x2F;span&gt;&lt;span&gt; chain_id;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The components of &lt;code&gt;pg_timetable&lt;&#x2F;code&gt; consist of the &lt;strong&gt;command&lt;&#x2F;strong&gt; (SQL&#x2F;program or built-in), &lt;strong&gt;task&lt;&#x2F;strong&gt; controlling the configuration for the command execution (error handling, timeout, or database connection to use), and &lt;strong&gt;chain&lt;&#x2F;strong&gt; which can contain a number of tasks chained together.&lt;&#x2F;p&gt;
&lt;p&gt;The big difference comes in the scheduling options. While &lt;code&gt;pg_cron&lt;&#x2F;code&gt; is limited (as its name suggests) to cron-like syntax, &lt;code&gt;pg_timetable&lt;&#x2F;code&gt; uses it for simple use cases but offers much more. It supports schedules &lt;code&gt;@every&lt;&#x2F;code&gt; and &lt;code&gt;@after&lt;&#x2F;code&gt;, allowing repeated execution and breaking away from the limitations of cron notation as it can go down to custom intervals (including second intervals). Another case is &lt;code&gt;@reboot&lt;&#x2F;code&gt; for instances when the &lt;code&gt;pg_timetable&lt;&#x2F;code&gt; controller reconnects to the database.&lt;&#x2F;p&gt;
&lt;p&gt;The timetable setup manages own schema migrations and stores all the configuration in schema &lt;code&gt;timetable&lt;&#x2F;code&gt; (by default), where you can find the definitions of chains&#x2F;tasks and relevant auditing information (logs).&lt;&#x2F;p&gt;
&lt;p&gt;And yes, if you have been paying attention, &lt;code&gt;pg_timetable&lt;&#x2F;code&gt; architecture allows for running tasks across multiple PostgreSQL clusters, making it an advanced orchestration tool. The database connection, which can be configured in-place or via a drop-in &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;libpq-pgservice.html&quot;&gt;connection service file&lt;&#x2F;a&gt;, can be set on a per-task basis.&lt;&#x2F;p&gt;
&lt;p&gt;With chains, the setup can be much more complex:&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;DO $$&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DECLARE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  v_chain_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BIGINT&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;  v_notify_task_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BIGINT&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;BEGIN&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 style=&quot;color: #79B8FF;&quot;&gt; timetable&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;chain&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;        chain_name,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        run_at,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        max_instances,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        live)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    VALUES&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;Generate Weekly Report&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;5 4 * * 1&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: #79B8FF;&quot;&gt;        1&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;        TRUE&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;    RETURNING chain_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INTO&lt;&#x2F;span&gt;&lt;span&gt; v_chain_id;&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 style=&quot;color: #79B8FF;&quot;&gt; timetable&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;task&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;        chain_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        task_order,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        task_name,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        command,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        database_connection&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;    VALUES&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;        v_chain_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&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: #6A737D;&quot;&gt;-- task_order&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;generate_report&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,                    &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- task_name&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;SELECT generate_weekly_report();&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,   &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- command&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        NULL&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;                                  -- database_connection&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 style=&quot;color: #79B8FF;&quot;&gt; timetable&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;task&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;        chain_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        task_order,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        task_name,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        command,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        database_connection&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;    VALUES&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;        v_chain_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&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: #6A737D;&quot;&gt;-- task_order (second task in the chain)&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;notify_management&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,                 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- task_name&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;SELECT pg_notify($1, $2)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,          &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- command&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;service=service_db&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;                 -- database_connection&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;    RETURNING task_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INTO&lt;&#x2F;span&gt;&lt;span&gt; v_notify_task_id;&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 style=&quot;color: #79B8FF;&quot;&gt; timetable&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;task_parameter&lt;&#x2F;span&gt;&lt;span&gt; (task_id, order_id, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;value&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;    VALUES&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        (v_notify_task_id, &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: #9ECBFF;&quot;&gt;&amp;#39;management_notifications&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;        (v_notify_task_id, &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: #9ECBFF;&quot;&gt;&amp;#39;Weekly report&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: #F97583;&quot;&gt;END&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;LANGUAGE&lt;&#x2F;span&gt;&lt;span&gt; plpgsql;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;the-comparison&quot;&gt;The Comparison&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-comparison&quot; aria-label=&quot;Anchor link for: the-comparison&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;When deciding between &lt;code&gt;pg_cron&lt;&#x2F;code&gt; and &lt;code&gt;pg_timetable&lt;&#x2F;code&gt;, it&#x27;s essential to consider the specific needs of your use case, as both tools target different scenarios and scales of deployment. Here’s a comparison to help you decide which tool is more suitable for your requirements:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Feature&lt;&#x2F;th&gt;&lt;th&gt;pg_cron&lt;&#x2F;th&gt;&lt;th&gt;pg_timetable&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Distribution&lt;&#x2F;td&gt;&lt;td&gt;PostgreSQL extension&lt;&#x2F;td&gt;&lt;td&gt;Standalone executable&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Scheduling&lt;&#x2F;td&gt;&lt;td&gt;Limited (cron-like)&lt;&#x2F;td&gt;&lt;td&gt;Extensive (intervals, calendar, custom)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Job Types&lt;&#x2F;td&gt;&lt;td&gt;SQL&lt;&#x2F;td&gt;&lt;td&gt;SQL, Built-in, Shell&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Job Chaining&lt;&#x2F;td&gt;&lt;td&gt;No&lt;&#x2F;td&gt;&lt;td&gt;Yes&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Error Handling&lt;&#x2F;td&gt;&lt;td&gt;Basic&lt;&#x2F;td&gt;&lt;td&gt;Intermediate&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Logging&lt;&#x2F;td&gt;&lt;td&gt;Basic&lt;&#x2F;td&gt;&lt;td&gt;Intermediate&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Dependencies&lt;&#x2F;td&gt;&lt;td&gt;No&lt;&#x2F;td&gt;&lt;td&gt;Yes&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Ease of Use&lt;&#x2F;td&gt;&lt;td&gt;Easy&lt;&#x2F;td&gt;&lt;td&gt;Moderate&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Configuration&lt;&#x2F;td&gt;&lt;td&gt;Easy&lt;&#x2F;td&gt;&lt;td&gt;Moderate&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Orchestration&lt;&#x2F;td&gt;&lt;td&gt;Single Cluster&lt;&#x2F;td&gt;&lt;td&gt;Advanced (multiple clusters)&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;h3 id=&quot;when-to-use-pg-cron&quot;&gt;When to Use &lt;code&gt;pg_cron&lt;&#x2F;code&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#when-to-use-pg-cron&quot; aria-label=&quot;Anchor link for: when-to-use-pg-cron&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;If your requirements are straightforward, such as running maintenance tasks, refreshing materialised views, or generating periodic reports using simple SQL commands, &lt;code&gt;pg_cron&lt;&#x2F;code&gt; is an excellent choice. Its cron-like syntax is familiar and &lt;strong&gt;easy to use&lt;&#x2F;strong&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pg_cron&lt;&#x2F;code&gt; is a PostgreSQL extension, making it &lt;strong&gt;easier to install&lt;&#x2F;strong&gt; and configure within your existing PostgreSQL environment. This makes it ideal for users who prefer minimal setup effort.&lt;&#x2F;li&gt;
&lt;li&gt;When your environment is limited to a &lt;strong&gt;single PostgreSQL cluster&lt;&#x2F;strong&gt;, &lt;code&gt;pg_cron&lt;&#x2F;code&gt; is well-suited for the job. It does not support orchestration across multiple clusters, so it’s best used in environments where all operations are confined to one database cluster.&lt;&#x2F;li&gt;
&lt;li&gt;If &lt;strong&gt;Basic Error Handling and Logging&lt;&#x2F;strong&gt; is sufficient, &lt;code&gt;pg_cron&lt;&#x2F;code&gt; provides the necessary functionalities without additional complexity.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h3 id=&quot;when-to-use-pg-timetable&quot;&gt;When to Use &lt;code&gt;pg_timetable&lt;&#x2F;code&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#when-to-use-pg-timetable&quot; aria-label=&quot;Anchor link for: when-to-use-pg-timetable&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;If you need more &lt;strong&gt;advanced scheduling capabilities&lt;&#x2F;strong&gt; and &lt;strong&gt;task depedencies and chaining&lt;&#x2F;strong&gt;, such as task chaining, complex intervals, and custom execution times, &lt;code&gt;pg_timetable&lt;&#x2F;code&gt; is the better choice. It supports extensive scheduling options beyond the typical cron syntax.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;pg_timetable&lt;&#x2F;code&gt; supports a &lt;strong&gt;variety of job types&lt;&#x2F;strong&gt;, including SQL commands, built-in tasks (like sending emails or downloading files), and shell commands. This makes it ideal for more complex automation needs that go beyond simple SQL execution.&lt;&#x2F;li&gt;
&lt;li&gt;If your environment spans multiple PostgreSQL clusters, &lt;code&gt;pg_timetable&lt;&#x2F;code&gt; can handle &lt;strong&gt;advanced orchestration&lt;&#x2F;strong&gt;, allowing tasks to be executed across different clusters seamlessly.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Intermediate Error Handling and Logging&lt;&#x2F;strong&gt; provides better insights and control over job executions.&lt;&#x2F;li&gt;
&lt;li&gt;Although bit more complex to setup, &lt;code&gt;pg_timetable&lt;&#x2F;code&gt; works as &lt;strong&gt;standalone Service&lt;&#x2F;strong&gt;, which makes it suitable for environments where extensions cannot be installed directly.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Choosing between &lt;code&gt;pg_cron&lt;&#x2F;code&gt; and &lt;code&gt;pg_timetable&lt;&#x2F;code&gt; depends on your specific needs. For simpler, single-cluster tasks, &lt;code&gt;pg_cron&lt;&#x2F;code&gt; offers ease of use and straightforward setup. For more complex requirements involving advanced scheduling, task dependencies, and multi-cluster orchestration, &lt;code&gt;pg_timetable&lt;&#x2F;code&gt; provides a more powerful and flexible solution.&lt;&#x2F;p&gt;
&lt;p&gt;By understanding the strengths and limitations of each tool, you can make an informed decision that best suits your database automation needs.&lt;&#x2F;p&gt;
&lt;p&gt;PS: there&#x27;s also &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pgadmin-org&#x2F;pgagent&quot;&gt;pgAgent&lt;&#x2F;a&gt;, but it is generally not used and does not seem to be actively maintained.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Deep Dive into PostgREST - Time Off Manager (Part 3)</title>
        <published>2024-06-06T00:00:00+00:00</published>
        <updated>2024-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/postgrest-tutorial-part3/"/>
        <id>https://boringsql.com/posts/postgrest-tutorial-part3/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/postgrest-tutorial-part3/">&lt;p&gt;This is the third and final instalment of &quot;Deep Dive into PostgREST&quot;. In the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;boringsql.com&#x2F;posts&#x2F;postgrest-tutorial-part1&#x2F;&quot;&gt;first part&lt;&#x2F;a&gt;, we explored basic CRUD functionalities. In the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;boringsql.com&#x2F;posts&#x2F;postgrest-tutorial-part2&#x2F;&quot;&gt;second part&lt;&#x2F;a&gt;, we moved forward with abstraction and used the acquired knowledge to create a simple request&#x2F;approval workflow.&lt;&#x2F;p&gt;
&lt;p&gt;In Part 3, we will explore authentication and authorisation options to finish something that might resemble a real-world application.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;authentication-with-pgcrypto&quot;&gt;Authentication with pgcrypto&lt;a class=&quot;zola-anchor&quot; href=&quot;#authentication-with-pgcrypto&quot; aria-label=&quot;Anchor link for: authentication-with-pgcrypto&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;There&#x27;s no authorisation without knowing user identity. So let&#x27;s start there. Our users table from the first part had an &lt;code&gt;email&lt;&#x2F;code&gt; to establish the identity, but no way to verify it. We will address this by adding a password column. Of course, nobody in their right mind would store passwords in plain text.&lt;&#x2F;p&gt;
&lt;p&gt;To securely store users&#x27; passwords, we are going to utilise PostgreSQL &lt;code&gt;pgcrypto&lt;&#x2F;code&gt; extension (&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;pgcrypto.html&quot;&gt;documentation&lt;&#x2F;a&gt;). This built-in extension provides a suite of cryptographic functions for hashing, encryption, and more. In our case, we will leverage &lt;code&gt;crypt&lt;&#x2F;code&gt; with the &lt;code&gt;gen_salt&lt;&#x2F;code&gt; function to generate password hash using bcrypt algorithm.&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s start with loading the extension and adding password column:&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 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IF NOT EXISTS&lt;&#x2F;span&gt;&lt;span&gt; pgcrypto;&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; users &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ADD&lt;&#x2F;span&gt;&lt;span&gt; COLUMN password_hash &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TEXT NOT NULL DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; gen_salt(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;bf&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;The new column &lt;code&gt;password_hash&lt;&#x2F;code&gt; will accommodate the variable-length bcrypt hash, while default function &lt;code&gt;gen_salt(&#x27;bf&#x27;)&lt;&#x2F;code&gt; creates a unique bcrypt salt for every user.&lt;&#x2F;p&gt;
&lt;p&gt;Now that our table structure is set, let&#x27;s see how we can securely set and verify passwords using pgcrypto.&lt;&#x2F;p&gt;
&lt;p&gt;When a user sets or changes their password, we&#x27;ll hash it using crypt before storing it in the password_hash column.Here&#x27;s how:&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;UPDATE users SET password_hash = crypt(&amp;#39;new_password&amp;#39;, gen_salt(&amp;#39;bf&amp;#39;)) WHERE email = &amp;#39;user@example.com&amp;#39;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and during login, we will verify the hash of the password the user enters with the stored password_hash:&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; user_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; users &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; email &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;user@example.com&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AND&lt;&#x2F;span&gt;&lt;span&gt; password_hash &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; crypt(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;entered_password&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, password_hash);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;jwt-authentication&quot;&gt;JWT Authentication&lt;a class=&quot;zola-anchor&quot; href=&quot;#jwt-authentication&quot; aria-label=&quot;Anchor link for: jwt-authentication&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;With the ability to securely verify users&#x27; identity, let&#x27;s move to the next step and build stateless authentication. The de-facto standard is JSON Web Tokens (JWTs). Represented by digitally signed information, they contain claims about user identity. The digital signature ensures the token&#x27;s integrity and verifies that it hasn&#x27;t been tampered with. You can find more in official &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;jwt.io&#x2F;introduction&quot;&gt;Introduction to JSON Web Tokens&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;While PostgreSQL doesn&#x27;t have built-in JWT support, we will have to either rely on &lt;code&gt;pgjwt&lt;&#x2F;code&gt; extension (&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;michelp&#x2F;pgjwt&quot;&gt;GitHub repo&lt;&#x2F;a&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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;CREATE EXTENSION IF NOT EXISTS pgjwt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;or you can re-create the logic by including &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;michelp&#x2F;pgjwt&#x2F;blob&#x2F;master&#x2F;pgjwt--0.2.0.sql&quot;&gt;PL&#x2F;pgSQL that comes with it&lt;&#x2F;a&gt; (please, make sure you replace&#x2F;remove the &lt;code&gt;@extschema@&lt;&#x2F;code&gt; to match the schema you are using). In this article we will use schema &lt;code&gt;jwt&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;To make the token signature, we need to re-configure PostgREST to decode JWT tokens and configure the secret inside the database (to be able to use it in our code). For security reasons, the key must be at least 32 characters long. You can either use your own method to generate (for example, your password manager) or add it using the following CLI commands&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: #F97583;&quot;&gt;export&lt;&#x2F;span&gt;&lt;span&gt; LC_CTYPE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;C&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;echo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;jwt-secret = &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;\&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;$(&lt;&#x2F;span&gt;&lt;span&gt;LC_ALL&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;C&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; tr&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -dc&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;A-Za-z0-9&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&#x2F;dev&#x2F;urandom&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; |&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; head&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -c32&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;\&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; postgrest.conf&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And use the generated secret for the database level configuration parameter, 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;ALTER DATABASE&lt;&#x2F;span&gt;&lt;span&gt; time_off_manager &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;pgrst.jwt_secret&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; to&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;your-jwt-secret-generated-above1&amp;quot;&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;Please, make sure the database name is updated accordingly to set it correctly. And, I cannot stress it enough, &lt;strong&gt;make sure the secret is really 32 characters&lt;&#x2F;strong&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-web-user-role&quot;&gt;The web user role&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-web-user-role&quot; aria-label=&quot;Anchor link for: the-web-user-role&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;In previous parts of this guide, we have worked with two user roles. The first, in PostgREST terminology, called authenticator, is the role used in the &lt;code&gt;db-uri&lt;&#x2F;code&gt; parameter. It&#x27;s the role used to access the database and its job is to impersonate other users based on the authentication (or lack thereof) of the HTTP requests. In a real production application, this role should be configured to have limited access.&lt;&#x2F;p&gt;
&lt;p&gt;The second role, called anonymous, is used in &lt;code&gt;db-anon-role&lt;&#x2F;code&gt; parameter. This is the role impersonated for all unauthenticated HTTP requests.&lt;&#x2F;p&gt;
&lt;p&gt;The third role, or roles, representing authenticated web users. In our JWT tokens, we will use one specifically designed for PostgREST.&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;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	&amp;quot;role&amp;quot;: &amp;quot;time_off_user&amp;quot;&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;When JWT is successfully validated, with a role claim, PostgREST will switch to the database role with the provided name for the duration of the HTTP request. While PostgREST is quite flexible, we will limit the impersonated role for authenticated requests to a single hard-coded role. For our application, it&#x27;s going to be called &lt;code&gt;time_off_user&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;let-s-generate-some-jwts&quot;&gt;Let&#x27;s generate some JWTs&lt;a class=&quot;zola-anchor&quot; href=&quot;#let-s-generate-some-jwts&quot; aria-label=&quot;Anchor link for: let-s-generate-some-jwts&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The first step is to implement logic to create a JWT token asserting all &lt;strong&gt;claims&lt;&#x2F;strong&gt; we need for our application - in the case of Time Off Manager, we will rely on user_id and role. As discussed in the previous section, we will use the hard-coded role &lt;code&gt;time_off_user&lt;&#x2F;code&gt;. Let&#x27;s create the role and grant our authenticator the permissions.&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 ROLE&lt;&#x2F;span&gt;&lt;span&gt; time_off_user NOLOGIN;&lt;&#x2F;span&gt;&lt;&#x2F;span&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; time_off_user &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; time_off_manager;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Now create a function, users can call directly to verify the identity and generate the JWT token.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plsql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE OR REPLACE&lt;&#x2F;span&gt;&lt;span&gt; FUNCTION api.login(email text, password text)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  RETURNS text&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  LANGUAGE&lt;&#x2F;span&gt;&lt;span&gt; plpgsql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  SECURITY DEFINER&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; $function$&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DECLARE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  user_record public.users;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BEGIN&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 * INTO&lt;&#x2F;span&gt;&lt;span&gt; user_record&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; public.users&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  WHERE&lt;&#x2F;span&gt;&lt;span&gt; users.email &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; login.email;&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;  IF&lt;&#x2F;span&gt;&lt;span&gt; user_record.password_hash &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; crypt(password, user_record.password_hash) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;	RETURN&lt;&#x2F;span&gt;&lt;span&gt; create_jwt(user_record.user_id, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;user_id&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: #F97583;&quot;&gt;  ELSE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;	RAISE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; EXCEPTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Invalid email or password&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: #F97583;&quot;&gt;  END IF&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;END&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;$function$;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Missing there is helper &lt;code&gt;create_jwt&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;plsql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE OR REPLACE&lt;&#x2F;span&gt;&lt;span&gt; FUNCTION public.create_jwt(user_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;integer&lt;&#x2F;span&gt;&lt;span&gt;, role text)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; RETURNS text&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; LANGUAGE&lt;&#x2F;span&gt;&lt;span&gt; plpgsql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; $function$&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DECLARE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  payload JSON;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BEGIN&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;:=&lt;&#x2F;span&gt;&lt;span&gt; json_build_object(&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;user_id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, user_id,&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;role&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;time_off_user&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;exp&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;extract&lt;&#x2F;span&gt;&lt;span&gt;(epoch &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; now()) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;+&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 3600&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; -- 1-hour expiration&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;  RETURN&lt;&#x2F;span&gt;&lt;span&gt; jwt.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;sign&lt;&#x2F;span&gt;&lt;span&gt;(payload, current_setting(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;pgrst.jwt_secret&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;));  &lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Use configuration value&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;END&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;$function$;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Restart the PostgREST instance to apply the changes, and you can try to generate a token using cURL (assuming you have changed the password as demonstrated in the first section ):&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;curl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -X&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; POST &amp;quot;http:&#x2F;&#x2F;localhost:3000&#x2F;rpc&#x2F;login&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&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;	-d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;{&amp;quot;email&amp;quot;:&amp;quot;manager2@example.com&amp;quot;, &amp;quot;password&amp;quot;: &amp;quot;new_password&amp;quot;}&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&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;	-H&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Content-Type: application&#x2F;json&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If you set everything as expected you will get a base64 encoded token. To verify&#x2F;debug it, you can try to use &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;jwt.io&#x2F;&quot;&gt;JWT.io&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;prepare-the-permissions&quot;&gt;Prepare the permissions&lt;a class=&quot;zola-anchor&quot; href=&quot;#prepare-the-permissions&quot; aria-label=&quot;Anchor link for: prepare-the-permissions&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The next step is where things get really interesting. First, we need to clean up excessive permissions. Some obvious, some less so.&lt;&#x2F;p&gt;
&lt;p&gt;First let&#x27;s start with the revoking all the permissions for &lt;code&gt;time_off_anonymous&lt;&#x2F;code&gt; we provided in previous part.&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;REVOKE&lt;&#x2F;span&gt;&lt;span&gt; ALL PRIVILEGES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span&gt; ALL TABLES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; api &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; time_off_anonymous;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;REVOKE EXECUTE ON&lt;&#x2F;span&gt;&lt;span&gt; ALL FUNCTIONS &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; api &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; time_off_anonymous;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and same we need to adjust the default privileges&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 DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; PRIVILEGES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; api &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;REVOKE SELECT ON&lt;&#x2F;span&gt;&lt;span&gt; TABLES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; time_off_anonymous;&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 DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; PRIVILEGES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; api &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;REVOKE EXECUTE ON&lt;&#x2F;span&gt;&lt;span&gt; FUNCTIONS &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; time_off_anonymous;&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;Which is the obvious part. The most likely surprising part is the need to remove &lt;code&gt;EXECUTE&lt;&#x2F;code&gt; permissions for &lt;code&gt;PUBLIC&lt;&#x2F;code&gt;. In PostgreSQL, granting usage on a schema also grants the ability to execute functions to &lt;code&gt;PUBLIC&lt;&#x2F;code&gt;. Unless you revoke these privileges, all users will be able to execute the functions.&lt;&#x2F;p&gt;
&lt;p&gt;For our purposes we want to REVOKE the access for &lt;code&gt;PUBLIC&lt;&#x2F;code&gt; and only grant &lt;code&gt;EXECUTE&lt;&#x2F;code&gt; on function &lt;code&gt;api.login()&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;GRANT&lt;&#x2F;span&gt;&lt;span&gt; USAGE &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; api &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; time_off_user;&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;-- REVOKE EXECUTE&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 DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; PRIVILEGES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; api &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;REVOKE EXECUTE ON&lt;&#x2F;span&gt;&lt;span&gt; FUNCTIONS &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; PUBLIC;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;REVOKE EXECUTE ON&lt;&#x2F;span&gt;&lt;span&gt; ALL FUNCTIONS &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; api &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; PUBLIC;&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;-- GRANT EXECUTE &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GRANT EXECUTE ON FUNCTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; api&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;login&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; to&lt;&#x2F;span&gt;&lt;span&gt; time_off_anonymous;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And finally, we need to establish the necessary permissions for &lt;code&gt;time_off_user&lt;&#x2F;code&gt;, the role which will be used for authenticated requests.&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 SELECT ON&lt;&#x2F;span&gt;&lt;span&gt; ALL TABLES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; public &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; time_off_user;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GRANT SELECT ON&lt;&#x2F;span&gt;&lt;span&gt; ALL TABLES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; api &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; time_off_user;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GRANT EXECUTE ON&lt;&#x2F;span&gt;&lt;span&gt; ALL FUNCTIONS &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; api &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; time_off_user;&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;ALTER DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; PRIVILEGES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; public &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GRANT SELECT ON&lt;&#x2F;span&gt;&lt;span&gt; TABLES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; time_off_user;&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 DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; PRIVILEGES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; api &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GRANT SELECT ON&lt;&#x2F;span&gt;&lt;span&gt; TABLES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; time_off_user;&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 DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; PRIVILEGES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; api &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GRANT EXECUTE ON&lt;&#x2F;span&gt;&lt;span&gt; FUNCTIONS &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; time_off_user;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We will now use this to get an overview of only relevant vacation balances for the authenticated user.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;authorization-with-postgrest&quot;&gt;Authorization with PostgREST&lt;a class=&quot;zola-anchor&quot; href=&quot;#authorization-with-postgrest&quot; aria-label=&quot;Anchor link for: authorization-with-postgrest&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;With authentication working, let&#x27;s start with the implementation of fine-grained authorization in PostgREST. As a first step, the goal is to ensure that users only see the vacation balances they are supposed to. I.e. either their own (for the regular employees) or their own and the people the user manages (for the managers).&lt;&#x2F;p&gt;
&lt;p&gt;For this we will need to access JWT token claims, specifically &lt;code&gt;user_id&lt;&#x2F;code&gt;. To avoid relatively complex notation, let&#x27;s setup a helper&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 OR REPLACE FUNCTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.current_user_id()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; RETURNS integer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; LANGUAGE&lt;&#x2F;span&gt;&lt;span&gt; plpgsql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; SECURITY&lt;&#x2F;span&gt;&lt;span&gt; DEFINER&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; $&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&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;DECLARE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    user_id &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: #F97583;&quot;&gt;BEGIN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    user_id :&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; current_setting(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;request.jwt.claims&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, true)::&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;json-&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;user_id&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;&#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; user_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IS NULL THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        RAISE EXCEPTION &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;User ID not found in JWT claims&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: #F97583;&quot;&gt;    END IF&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;    RETURN&lt;&#x2F;span&gt;&lt;span&gt; user_id::&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: #F97583;&quot;&gt;END&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;function&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;PostgREST seamlessly integrates with PostgreSQL&#x27;s Row Level Security (RLS) to filter data based on user permissions. RLS policies are rules applied at the row level to determine if a user can access a particular row in a table.&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 TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;time_off_transactions&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ENABLE ROW LEVEL SECURITY&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 POLICY&lt;&#x2F;span&gt;&lt;span&gt; select_own_balance &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;time_off_transactions&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;FOR SELECT USING&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;current_user_id&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; user_id);&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 POLICY&lt;&#x2F;span&gt;&lt;span&gt; select_supervised_balance &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;time_off_transactions&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FOR SELECT USING&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;EXISTS&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 style=&quot;color: #79B8FF;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; FROM&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; api&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;users&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; WHERE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; users&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;user_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; time_off_transactions&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;user_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AND&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; users&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;manager_user_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;current_user_id&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;If you are (and I do hope so) using PostgreSQL 15 and higher, you need to switch the view from the default security definer to invoker.&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 VIEW&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; api&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;vacation_balances&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; SET&lt;&#x2F;span&gt;&lt;span&gt; (security_invoker &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; true);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This way the view, and the underlying tables will be evaluated using the permissions of the user querying the view, not the view owner. This is the behaviour we want. For versions beyond PostgreSQL 15 and more complex use cases, you might need to implement function-based security instead.&lt;&#x2F;p&gt;
&lt;p&gt;But without further delay, let&#x27;s test &quot;the magic&quot; and (assuming you have authenticated as a manager) you might try to retrieve the vacation balances using cURL&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;curl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;http:&#x2F;&#x2F;localhost:3000&#x2F;vacation_balances&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&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;	-H&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Authorization: Bearer ${&lt;&#x2F;span&gt;&lt;span&gt;JWT_TOKEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;}&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;to get a result similar to&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;&amp;quot;year&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2024&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;&amp;quot;user_id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;8&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;&amp;quot;total_amount&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;25&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;&amp;quot;year&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2024&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;&amp;quot;user_id&amp;quot;&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: #79B8FF;&quot;&gt;&amp;quot;total_amount&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;25&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;&amp;quot;year&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2024&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;&amp;quot;user_id&amp;quot;&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;&amp;quot;total_amount&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;25&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;&amp;quot;year&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2024&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;&amp;quot;user_id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;11&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;&amp;quot;total_amount&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;25&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;&amp;quot;year&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2024&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;&amp;quot;user_id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;12&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;&amp;quot;total_amount&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;25&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;&amp;quot;year&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2024&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;&amp;quot;user_id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;13&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;&amp;quot;total_amount&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;25&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;Guess, it&#x27;s important to mention the Row Security Policies which are used are much more complex, and the whole topic would deserve another article. For now, I do recommend you to &lt;del&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;ddl-rowsecurity.html&quot;&gt;consult the documentation&lt;&#x2F;a&gt;&lt;&#x2F;del&gt;, to get more understanding. With the multi-role access you can implement with PostgREST, it might be interesting for you to focus on &lt;code&gt;BYPASSRLS&lt;&#x2F;code&gt; which might be applied to certain role(s).&lt;&#x2F;p&gt;
&lt;p&gt;As mentioned before, Row Level Security is just one way to solve this. The traditional approach would be to use functions to hide the separation logic.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wrapping-up-the-time-off-manager&quot;&gt;Wrapping up the Time Off Manager&lt;a class=&quot;zola-anchor&quot; href=&quot;#wrapping-up-the-time-off-manager&quot; aria-label=&quot;Anchor link for: wrapping-up-the-time-off-manager&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Before we wrap up our tutorial, let&#x27;s finish the core functionality of the Time Off Manager - the approval workflow. Given the basic concepts covered above, this is going to be a good exercise to use all of it.&lt;&#x2F;p&gt;
&lt;p&gt;Similar how we updated &lt;code&gt;time_off_transactions&lt;&#x2F;code&gt;, we will apply Row Level Security to &lt;code&gt;time_off_requests&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 TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;time_off_requests&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ENABLE ROW LEVEL SECURITY&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 POLICY&lt;&#x2F;span&gt;&lt;span&gt; select_own_requests &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;time_off_requests&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;FOR SELECT USING&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;current_user_id&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; user_id); &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 POLICY&lt;&#x2F;span&gt;&lt;span&gt; select_subordinate_requests &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;time_off_requests&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FOR SELECT USING&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;EXISTS&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 style=&quot;color: #79B8FF;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; FROM&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;users&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; WHERE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; users&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;user_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; time_off_requests&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;user_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AND&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; users&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;manager_user_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;current_user_id&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;Don&#x27;t forget to switch the view to &lt;code&gt;security_invoker&lt;&#x2F;code&gt; model.&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 VIEW&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; api&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;pending_requests&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; SET&lt;&#x2F;span&gt;&lt;span&gt; (security_invoker &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; true);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And we wouldn&#x27;t be done with requests creation, if we wouldn&#x27;t update function &lt;code&gt;api.request_time_off&lt;&#x2F;code&gt; to take the advantage of the newly propagated &lt;code&gt;user_id&lt;&#x2F;code&gt; from authentication.&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 OR REPLACE FUNCTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; api&lt;&#x2F;span&gt;&lt;span&gt;.request_time_off(leave_type &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;period&lt;&#x2F;span&gt;&lt;span&gt; daterange)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;RETURNS integer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LANGUAGE&lt;&#x2F;span&gt;&lt;span&gt; plpgsql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SECURITY&lt;&#x2F;span&gt;&lt;span&gt; DEFINER&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; $&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&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;DECLARE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    v_leave_type_id &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;    v_request_id &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 style=&quot;color: #F97583;&quot;&gt;BEGIN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    -- original validation logic &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;    -- Ensure the user is requesting for themselves&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 style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;current_user_id&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; request_time_off&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;user_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        RAISE EXCEPTION &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;User can only request time off for themselves&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: #F97583;&quot;&gt;    END IF&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;    -- the rest of the function&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;END&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;function&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;The similar update then applies to the &lt;code&gt;api.update_request&lt;&#x2F;code&gt; function to ensure only managers can approve&#x2F;reject time off requests.&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 OR REPLACE FUNCTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; api&lt;&#x2F;span&gt;&lt;span&gt;.update_request(request_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;integer&lt;&#x2F;span&gt;&lt;span&gt;, new_status &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;RETURNS&lt;&#x2F;span&gt;&lt;span&gt; void&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LANGUAGE&lt;&#x2F;span&gt;&lt;span&gt; plpgsql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SECURITY&lt;&#x2F;span&gt;&lt;span&gt; DEFINER&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; $&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&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;DECLARE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    v_requested_user_id &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;    v_request_manager_id &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 style=&quot;color: #F97583;&quot;&gt;BEGIN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    -- the original code up to the retrival of the requests&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;    IF&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;current_user_id&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; v_request_manager_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        RAISE EXCEPTION &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Only the manager can approve or reject this request&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: #F97583;&quot;&gt;    END IF&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;    -- update the request status&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;END&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;function&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 about it! Obviously, we can&#x27;t pretend Time Off Manager is anywhere complete, and the real-life application would require much more than what we have covered. But it should provide a good training platform to allow you to write a real API-based backends using just PostgREST.&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;em&gt;Correction 2024-06-11&lt;&#x2F;em&gt;: during the final edits the function &lt;code&gt;public.current_user_id&lt;&#x2F;code&gt; somehow got missing from the Markdown [FIXED].&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>Deep Dive into PostgREST - Time Off Manager (Part 2)</title>
        <published>2024-05-18T00:00:00+00:00</published>
        <updated>2024-05-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/postgrest-tutorial-part2/"/>
        <id>https://boringsql.com/posts/postgrest-tutorial-part2/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/postgrest-tutorial-part2/">&lt;p&gt;Let&#x27;s recap the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;boringsql.com&#x2F;posts&#x2F;postgrest-tutorial-part1&#x2F;&quot;&gt;first part&lt;&#x2F;a&gt; of &quot;Deep Dive into PostgREST,&quot; where we explored the basic functionality to expose and query any table using an API, demonstrated using &lt;code&gt;cURL&lt;&#x2F;code&gt;. All it took was to set up a &lt;code&gt;db-schema&lt;&#x2F;code&gt; and give the &lt;code&gt;db-anon-role&lt;&#x2F;code&gt; some permissions. But unless you are creating the simplest of CRUD applications, this only scratches the surface.&lt;&#x2F;p&gt;
&lt;p&gt;In Part 2, we will expand APIs, provide better abstraction, and implement the foundation of what can be considered business logic, all while extending the sample &quot;Time Off Manager&quot; application. While the previous instalment being introductiory only, make sure you don&#x27;t miss the important details in this one.&lt;&#x2F;p&gt;
&lt;p&gt;Before we move on, let&#x27;s do a bit of housekeeping and clean up the permissions setup from the first part. This way, we can start with a clean slate (when it comes to the permissions) and avoid any lingering rights that could confuse us later.&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;REVOKE&lt;&#x2F;span&gt;&lt;span&gt; ALL PRIVILEGES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span&gt; ALL TABLES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; public &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; time_off_anonymous;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;dedicated-schema-for-the-api&quot;&gt;Dedicated schema for the API&lt;a class=&quot;zola-anchor&quot; href=&quot;#dedicated-schema-for-the-api&quot; aria-label=&quot;Anchor link for: dedicated-schema-for-the-api&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Using the &lt;code&gt;public&lt;&#x2F;code&gt; schema (or any schema(s) where your core data model resides) is a fast way to get started. However, using a dedicated schema for the API is beneficial for several reasons:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;It provides options for better abstraction, which you might appreciate later when refactoring the original data model.&lt;&#x2F;li&gt;
&lt;li&gt;Data customisation is also a requirement unless you prefer building a &quot;fat&quot; client and thus transferring the majority of the business logic there. Combining data from multiple tables helps shield the consumer from complex queries and relations.&lt;&#x2F;li&gt;
&lt;li&gt;While we won&#x27;t explore it as a security feature, it can also provide relevant boundaries.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Let&#x27;s get started with the creation of the schema itself, and expose the users using a view and setting basic permissions. We will do this by setting default permissions, so we don&#x27;t have to repeat the same for all objects as we create them. Please note that default privileges are applied only to new objects created.&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 SCHEMA&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; api&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;GRANT&lt;&#x2F;span&gt;&lt;span&gt; USAGE &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; api &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; time_off_anonymous;&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 DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; PRIVILEGES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; api &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GRANT SELECT ON&lt;&#x2F;span&gt;&lt;span&gt; TABLES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; time_off_anonymous;&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 DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; PRIVILEGES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN SCHEMA&lt;&#x2F;span&gt;&lt;span&gt; api &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GRANT EXECUTE ON&lt;&#x2F;span&gt;&lt;span&gt; FUNCTIONS &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; time_off_anonymous;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Ok, let&#x27;s create our first view:&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 VIEW&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; api&lt;&#x2F;span&gt;&lt;span&gt;.users &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    u&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;user_id&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;    u&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;email&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;    m&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;user_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; manager_user_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    m&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;email&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS&lt;&#x2F;span&gt;&lt;span&gt; manager_email,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    u&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&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;FROM&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;    public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;users&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS&lt;&#x2F;span&gt;&lt;span&gt; u&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    LEFT JOIN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;users&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AS&lt;&#x2F;span&gt;&lt;span&gt; m &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; u&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;manager_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; m&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;user_id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    u&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;deleted_at&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; is null&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 way, we established the foundation for listing users while providing much richer information about a person&#x27;s manager and preventing potential additional roundtrips between client and API. We also included simple business logic—by omitting the soft-deleted employees.&lt;&#x2F;p&gt;
&lt;p&gt;After creating the views, it&#x27;s important to update the &lt;code&gt;postgrest.conf&lt;&#x2F;code&gt; file to use the new &lt;code&gt;api&lt;&#x2F;code&gt; schema:&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;db-uri = &amp;quot;postgres:&#x2F;&#x2F;username:password@localhost&#x2F;time_off_manager&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;db-schema = &amp;quot;api&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;db-anon-role = &amp;quot;time_off_anonymous&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This change tells PostgREST to treat the &lt;code&gt;api&lt;&#x2F;code&gt; schema as the primary interface for API requests, rather than the public schema. Once done, feel free to restart the PostgREST server and test the new setup.&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;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; curl &amp;quot;http:&#x2F;&#x2F;localhost:3000&#x2F;users&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;We can further extend the example by provisioning a view with a summary of the vacation days available per calendar year (of course, for simplicity we won&#x27;t consider transfers between years, etc.).&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 VIEW&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; api&lt;&#x2F;span&gt;&lt;span&gt;.vacation_balances &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    EXTRACT(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;YEAR FROM&lt;&#x2F;span&gt;&lt;span&gt; transaction_date) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS year&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;    user_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    SUM&lt;&#x2F;span&gt;&lt;span&gt;(amount) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; total_amount&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; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;time_off_transactions&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;JOIN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; api&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;users&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; USING&lt;&#x2F;span&gt;&lt;span&gt; (user_id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&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;    leave_type_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&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; leave_type_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;leave_types&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; WHERE&lt;&#x2F;span&gt;&lt;span&gt; label &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;vacation&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: #F97583;&quot;&gt;GROUP BY&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;    EXTRACT(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;YEAR FROM&lt;&#x2F;span&gt;&lt;span&gt; transaction_date), user_id;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;While this is nothing groundbreaking, we could (and should) have certainly used views in the first part. So what makes this important now? As mentioned above, the main use cases are abstraction, data access, and security. Additional benefits might include other concerns like derived columns (e.g., full name if we were using first and last name columns), enriching data (e.g., building full URLs from fragments), and other logic specific to the presentation layer.&lt;&#x2F;p&gt;
&lt;p&gt;For practice, we can expose possible time off types via a simple view:&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 VIEW&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; api&lt;&#x2F;span&gt;&lt;span&gt;.leave_types &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;SELECT&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    label&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; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;leave_types&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; deleted_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;is null&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;let-s-modify-the-data&quot;&gt;Let&#x27;s modify the data&lt;a class=&quot;zola-anchor&quot; href=&quot;#let-s-modify-the-data&quot; aria-label=&quot;Anchor link for: let-s-modify-the-data&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;While the simplicity of performing CRUD operations on tables in the public schema was straightforward in the first part of the tutorial, using VIEWs introduces certain complexities. One key limitation of VIEWs is their restricted capability for data modification. Let&#x27;s look at how to encapsulate the logic.&lt;&#x2F;p&gt;
&lt;p&gt;You might have guessed it—the most obvious way forward is to introduce a database stored procedure for any business logic we want to expose.&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 OR REPLACE FUNCTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; api&lt;&#x2F;span&gt;&lt;span&gt;.add_user(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    email &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&gt;    manager_id &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;span style=&quot;color: #F97583;&quot;&gt;RETURNS integer AS&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;DECLARE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    v_new_user_id &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: #F97583;&quot;&gt;BEGIN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    -- check if email already exists&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    PERFORM &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; FROM&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; api&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;users&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; WHERE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; users&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;email&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; add_user&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;email&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; FOUND &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        RAISE EXCEPTION &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;The email address % is already in use&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;add_user&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;email&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;            USING&lt;&#x2F;span&gt;&lt;span&gt; ERRCODE &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;unique_violation&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: #F97583;&quot;&gt;    END IF&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 style=&quot;color: #6A737D;&quot;&gt;    -- sample business logic: all employees must have manager&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; manager_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IS NULL THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        RAISE EXCEPTION &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;manager_id must be provided and cannot be null&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: #F97583;&quot;&gt;    END IF&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 style=&quot;color: #F97583;&quot;&gt;    INSERT INTO&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;users&lt;&#x2F;span&gt;&lt;span&gt; (email, manager_id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;add_user&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;email&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;add_user&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;manager_id&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;    RETURNING user_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INTO&lt;&#x2F;span&gt;&lt;span&gt; v_new_user_id;&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;    RETURN&lt;&#x2F;span&gt;&lt;span&gt; v_new_user_id;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;END&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;LANGUAGE&lt;&#x2F;span&gt;&lt;span&gt; plpgsql &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SECURITY&lt;&#x2F;span&gt;&lt;span&gt; DEFINER;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Ok, we have introduced a rather complex piece of logic there. While it might look simple (if you&#x27;ve ever seen a PL&#x2F;pgSQL function), it comes with a couple of important details.&lt;&#x2F;p&gt;
&lt;p&gt;First and foremost, the function &lt;code&gt;api.add_user&lt;&#x2F;code&gt; presents an example of how to implement business logic. In this case, it&#x27;s providing a custom error message should the employee&#x27;s email already exist and enforcing the logic that &lt;code&gt;manager_id&lt;&#x2F;code&gt; must be provided. You can probably think of more cases to enrich data.&lt;&#x2F;p&gt;
&lt;p&gt;Now for the more difficult part—you might not be familiar with the &lt;code&gt;SECURITY&lt;&#x2F;code&gt; attribute. Every function created can have two modes, &lt;code&gt;INVOKER&lt;&#x2F;code&gt; (which is the default) and &lt;code&gt;DEFINER&lt;&#x2F;code&gt;. If you look above, when we removed all the permissions for our &lt;code&gt;db-anon-role&lt;&#x2F;code&gt; from schema &lt;code&gt;public&lt;&#x2F;code&gt;, the user lost privileges to perform any operations there. Without modifying the security attribute, the function would result in an error message &lt;code&gt;permission denied for table users&lt;&#x2F;code&gt;. Using &lt;code&gt;SECURITY DEFINER&lt;&#x2F;code&gt; guarantees your application user&#x27;s (the one used for the creating the view) permissions will be used when calling the function.&lt;&#x2F;p&gt;
&lt;p&gt;This is the opposite of how VIEWs work in this example, as they come with the default option &lt;code&gt;security_invoker&lt;&#x2F;code&gt; disabled by default. You can refer to the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;sql-createview.html&quot;&gt;documentation&lt;&#x2F;a&gt; to learn more.&lt;&#x2F;p&gt;
&lt;p&gt;If you are not familiar with PL&#x2F;pgSQL, I recommend you check the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;plpgsql-statements.html&quot;&gt;basic statements&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;With the function in place, let&#x27;s call it using cURL:&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;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; curl &amp;quot;http:&#x2F;&#x2F;localhost:3000&#x2F;rpc&#x2F;add_user&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -X&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; POST&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&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;	-d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;{ &amp;quot;email&amp;quot;: &amp;quot;admin2@example.com&amp;quot;, &amp;quot;manager_id&amp;quot;: 2 }&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&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;	-H&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Content-Type: application&#x2F;json&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is the second time we need to deconstruct the seemingly simple logic. Let&#x27;s start with the URL. The &lt;code&gt;&#x2F;rpc&#x2F;&lt;&#x2F;code&gt; prefix is important. Every stored procedure in the schema defined using &lt;code&gt;db-schema&lt;&#x2F;code&gt; is accessible under this prefix. The prefix helps to differentiate object types, as in PostgreSQL you are allowed to have the same name for a table&#x2F;view and a function.&lt;&#x2F;p&gt;
&lt;p&gt;The second part is the request method, in this case &lt;code&gt;POST&lt;&#x2F;code&gt;. While you can use both &lt;code&gt;GET&lt;&#x2F;code&gt; and &lt;code&gt;POST&lt;&#x2F;code&gt;, it&#x27;s important to understand the implications. Using the &lt;code&gt;GET&lt;&#x2F;code&gt; method sets the PostgREST transaction to read-only mode and is a powerful security measure—explicitly preventing the operation from performing any modification of the data (even indirectly via functions or triggers). Should you perform the function using &lt;code&gt;GET&lt;&#x2F;code&gt;, you would get a &lt;code&gt;cannot execute INSERT in a read-only transaction&lt;&#x2F;code&gt; error message to confirm it.&lt;&#x2F;p&gt;
&lt;p&gt;The third part of the call is the way the data is presented. In this example, it&#x27;s using JSON (declared as &lt;code&gt;Content-Type: application&#x2F;json&lt;&#x2F;code&gt; header) and passed within the request body. If required, you can choose other formats—like &lt;code&gt;application&#x2F;x-www-form-urlencoded&lt;&#x2F;code&gt;, &lt;code&gt;text&#x2F;xml&lt;&#x2F;code&gt;, &lt;code&gt;application&#x2F;octet-stream&lt;&#x2F;code&gt; for &lt;code&gt;bytea&lt;&#x2F;code&gt;, and more using &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;postgrest.org&#x2F;en&#x2F;latest&#x2F;api.html#custom-media-type-handlers&quot;&gt;custom media type handlers&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;Therefore, the same can be achieved 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;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; curl &amp;quot;http:&#x2F;&#x2F;localhost:3000&#x2F;rpc&#x2F;add_user&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -X&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; POST&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&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; 	-d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;email=admin2%40example.com&amp;amp;manager_id=2&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&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;	-H&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Content-Type: application&#x2F;x-www-form-urlencoded&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Let&#x27;s omit the XML example completely [sic].&lt;&#x2F;p&gt;
&lt;p&gt;As we have introduced the function that modifies the data, we should explore the option to do the same with read-only functions.&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 OR REPLACE FUNCTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; api&lt;&#x2F;span&gt;&lt;span&gt;.get_max_vacation_days()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;RETURNS INTEGER&lt;&#x2F;span&gt;&lt;span&gt; STABLE &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;    SELECT&lt;&#x2F;span&gt;&lt;span&gt; max_days&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; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;leave_types&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    WHERE&lt;&#x2F;span&gt;&lt;span&gt; label &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;vacation&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;$$ &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LANGUAGE sql SECURITY&lt;&#x2F;span&gt;&lt;span&gt; DEFINER;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Here, we defined the function &lt;code&gt;get_max_vacation_days&lt;&#x2F;code&gt;, which enables retrieving the current number of vacation days for all employees. To call the function, we can simply 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;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; curl &amp;quot;http:&#x2F;&#x2F;localhost:3000&#x2F;rpc&#x2F;get_max_vacation_days&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This summarises the basics of using a dedicated schema and VIEWs. While using stored procedures is the most obvious way, you can also use updatable views and INSTEAD OF TRIGGERS. This approach allows you to hide the possible transition from table to views and implement the insert logic using the trigger function. While this is a powerful technique, I recommend using direct function calls for clarity (at least when you are getting started).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;simple-workflow-management&quot;&gt;Simple workflow management&lt;a class=&quot;zola-anchor&quot; href=&quot;#simple-workflow-management&quot; aria-label=&quot;Anchor link for: simple-workflow-management&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;As we have introduced quite a lot of (potentially new) concepts, let&#x27;s practice more and get our hands dirty with the implementation of a simple workflow management system. In our case, it&#x27;s functionality for employees to request time off and manage approval flow. The premise is simple:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Employee can request a time off of a certain type&lt;&#x2F;li&gt;
&lt;li&gt;Employee can approve&#x2F;reject the time off request for their reports&lt;&#x2F;li&gt;
&lt;li&gt;The boss can do whatever they want&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Let&#x27;s start with defining the table for time off requests. Please notice we are going to create it again in schema &lt;code&gt;public&lt;&#x2F;code&gt; and not directly expose it via the API.&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; public&lt;&#x2F;span&gt;&lt;span&gt;.time_off_requests (&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    request_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;    user_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT REFERENCES&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;users&lt;&#x2F;span&gt;&lt;span&gt;(user_id),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    leave_type_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INT REFERENCES&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;leave_types&lt;&#x2F;span&gt;&lt;span&gt;(leave_type_id),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    requested_date &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DATE&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;    period&lt;&#x2F;span&gt;&lt;span&gt; DATERANGE,&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 CHECK&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;status IN&lt;&#x2F;span&gt;&lt;span&gt; (&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;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;approved&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;rejected&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;TIMESTAMP WITH TIME ZONE DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; current_timestamp&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;We will add functionality to request time off via a 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;CREATE OR REPLACE FUNCTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; api&lt;&#x2F;span&gt;&lt;span&gt;.request_time_off(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    user_id &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;    leave_type &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;    period&lt;&#x2F;span&gt;&lt;span&gt; DATERANGE&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;RETURNS INTEGER AS&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;DECLARE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    v_leave_type_id &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;    v_request_id &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 style=&quot;color: #F97583;&quot;&gt;BEGIN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    -- validate the leave type&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; leave_type_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INTO&lt;&#x2F;span&gt;&lt;span&gt; v_leave_type_id &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; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;leave_types&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;    WHERE&lt;&#x2F;span&gt;&lt;span&gt; label &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; request_time_off&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;leave_type&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 style=&quot;color: #F97583;&quot;&gt;    IF&lt;&#x2F;span&gt;&lt;span&gt; v_leave_type_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IS NULL THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        RAISE EXCEPTION &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Invalid leave type: %&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;request_time_off&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;leave_type&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;    END IF&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;    -- check if the user ID is valid&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    PERFORM &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; FROM&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;users&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; WHERE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; users&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;user_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; request_time_off&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;user_id&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 NOT&lt;&#x2F;span&gt;&lt;span&gt; FOUND &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        RAISE EXCEPTION &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Invalid user ID: %&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;request_time_off&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;user_id&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;    END IF&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 style=&quot;color: #6A737D;&quot;&gt;    -- insert the new time off request&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 style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;time_off_requests&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;        user_id, leave_type_id, requested_date, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;period&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;status&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;VALUES&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;        request_time_off&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;user_id&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;		v_leave_type_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;		CURRENT_DATE,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;		request_time_off&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;period&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;pending&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ) RETURNING request_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INTO&lt;&#x2F;span&gt;&lt;span&gt; v_request_id;&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;    RETURN&lt;&#x2F;span&gt;&lt;span&gt; v_request_id;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;END&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;LANGUAGE&lt;&#x2F;span&gt;&lt;span&gt; plpgsql &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SECURITY&lt;&#x2F;span&gt;&lt;span&gt; DEFINER;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For the purposes of this guide, let&#x27;s leave details out, and we would expect the &lt;code&gt;period&lt;&#x2F;code&gt; to be the applicable working days. To reiterate, the function above:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Provides basic application logic, setting the status to &#x27;pending&#x27;&lt;&#x2F;li&gt;
&lt;li&gt;Explicitly declares &lt;code&gt;SECURITY DEFINER&lt;&#x2F;code&gt; to facilitate permissions to access the newly created table.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;We can call it 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;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; curl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -X&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; POST &amp;quot;http:&#x2F;&#x2F;localhost:3000&#x2F;rpc&#x2F;request_time_off&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&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;	-d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;{&amp;quot;user_id&amp;quot;: 6, &amp;quot;leave_type&amp;quot;: &amp;quot;vacation&amp;quot;, &amp;quot;period&amp;quot;: &amp;quot;[2024-05-20,2024-05-21]&amp;quot;} &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&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;	-H&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Content-Type: application&#x2F;json&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;For the client to be able to display the pending requests, let&#x27;s introduce the view using the request fields and adding the manager ID of the employee, which we will utilise in the next step:&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 VIEW&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; api&lt;&#x2F;span&gt;&lt;span&gt;.pending_requests &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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 style=&quot;color: #79B8FF;&quot;&gt;    r&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;request_id&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;    r&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;user_id&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;    r&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;leave_type_id&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;    r&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;requested_date&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;    r&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;period&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;    r&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&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 style=&quot;color: #79B8FF;&quot;&gt;    r&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;created_at&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;    u&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;manager_id&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; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;time_off_requests&lt;&#x2F;span&gt;&lt;span&gt; r&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;JOIN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;users&lt;&#x2F;span&gt;&lt;span&gt; u &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; r&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;user_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; u&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;user_id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; r&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;status&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;pending&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ORDER BY&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; r&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;created_at&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;With this in place, we can move forward with implementing the function &lt;code&gt;api.update_request&lt;&#x2F;code&gt; to provide the necessary business logic to approve&#x2F;reject the requests.&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 OR REPLACE FUNCTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; api&lt;&#x2F;span&gt;&lt;span&gt;.update_request(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    request_id &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;    user_id &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;    new_status &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;span style=&quot;color: #F97583;&quot;&gt;RETURNS&lt;&#x2F;span&gt;&lt;span&gt; VOID &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;DECLARE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    v_requested_user_id &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;    v_request_manager_id &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 style=&quot;color: #F97583;&quot;&gt;BEGIN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    -- validate the new status&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; new_status &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;NOT IN&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;approved&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;rejected&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        RAISE EXCEPTION &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Invalid status: %. Only &amp;quot;approved&amp;quot; or &amp;quot;rejected&amp;quot; are allowed&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, new_status;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    END IF&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;    -- retrieve the request together with the users associated with it&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 style=&quot;color: #79B8FF;&quot;&gt; pr&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;user_id&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;pr&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;manager_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; INTO&lt;&#x2F;span&gt;&lt;span&gt; v_requested_user_id, v_request_manager_id&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; api&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;pending_requests&lt;&#x2F;span&gt;&lt;span&gt; pr&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    WHERE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; pr&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;request_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; update_request&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;request_id&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;    IF NOT&lt;&#x2F;span&gt;&lt;span&gt; FOUND &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        RAISE EXCEPTION &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;There&amp;#39;&amp;#39;s no pending Time off request ID %&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, request_id;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    END IF&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;    -- prevent users from self-approving their requests&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; user_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; v_requested_user_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        RAISE EXCEPTION &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;User cannot approve or reject their own request&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: #F97583;&quot;&gt;    END IF&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;    -- check if the user is either the requester’s manager or the boss&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; (v_request_manager_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IS NOT NULL AND&lt;&#x2F;span&gt;&lt;span&gt; user_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;lt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; v_request_manager_id) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;THEN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        RAISE EXCEPTION &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Only the manager or The Boss can approve or reject the request&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: #F97583;&quot;&gt;    END IF&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;    -- update the request status&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    UPDATE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;time_off_requests&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    SET status =&lt;&#x2F;span&gt;&lt;span&gt; new_status&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    WHERE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; time_off_requests&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;request_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; update_request&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;request_id&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;END&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;LANGUAGE&lt;&#x2F;span&gt;&lt;span&gt; plpgsql &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SECURITY&lt;&#x2F;span&gt;&lt;span&gt; DEFINER;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This time, the function shouldn&#x27;t have any surprises—just ensure you follow all the points addressed previously. With the functionality to approve&#x2F;reject the requests, we are missing the last crucial step: deducting the balance upon approval. There are two ways to go about this: either add the logic to the function above or implement triggers. While a full discussion goes beyond this guide, let&#x27;s provide a brief summary of when to choose which solution.&lt;&#x2F;p&gt;
&lt;p&gt;Choose a function if you have complex logic reusable by many functions (which does not prevent re-use in triggers), and use triggers if you prefer the automatic business rule enforcement and consistency on the database level. For this tutorial let&#x27;s do triggers to expand further the use of different SQL techniques (plus I would personally choose this solution anyway).&lt;&#x2F;p&gt;
&lt;p&gt;Before jumping in, let&#x27;s create a helper function that will calculate the number of days to deduct from the balance.&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 OR REPLACE FUNCTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.days_in_daterange(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;period&lt;&#x2F;span&gt;&lt;span&gt; daterange) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;RETURNS INT AS&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;    SELECT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; upper&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;period&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; lower&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;period&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;LANGUAGE sql&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;With it in place, we can create the 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;CREATE OR REPLACE FUNCTION&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.create_transaction_on_approval()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; RETURNS&lt;&#x2F;span&gt;&lt;span&gt; TRIGGER &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&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;BEGIN&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 style=&quot;color: #79B8FF;&quot;&gt; NEW&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;status&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;approved&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; THEN&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; time_off_transactions (&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            user_id, &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            leave_type_id, &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            transaction_date, &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            time_off_period, &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;&#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;VALUES&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;            NEW&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;user_id&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;            NEW&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;leave_type_id&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;            CURRENT_DATE,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            NEW&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;period&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: #79B8FF;&quot;&gt;public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;days_in_daterange&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;NEW&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;period&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 style=&quot;color: #F97583;&quot;&gt;    END IF&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 style=&quot;color: #F97583;&quot;&gt;    RETURN&lt;&#x2F;span&gt;&lt;span&gt; NEW;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;END&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;LANGUAGE&lt;&#x2F;span&gt;&lt;span&gt; plpgsql;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And set up the trigger itself:&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 TRIGGER&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; create_transaction_on_approval&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AFTER UPDATE ON&lt;&#x2F;span&gt;&lt;span&gt; time_off_requests&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FOR&lt;&#x2F;span&gt;&lt;span&gt; EACH &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ROW&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHEN&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;NEW&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;status&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;approved&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; AND&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; OLD&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;status&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;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 style=&quot;color: #F97583;&quot;&gt;EXECUTE FUNCTION&lt;&#x2F;span&gt;&lt;span&gt; create_transaction_on_approval();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;With the approval logic in place, we can try the original function via API:&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;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; curl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -X&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; POST &amp;quot;http:&#x2F;&#x2F;localhost:3000&#x2F;rpc&#x2F;update_request&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&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;	-d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;{&amp;quot;request_id&amp;quot;: 1, &amp;quot;user_id&amp;quot;: 2, &amp;quot;new_status&amp;quot;: &amp;quot;approved&amp;quot;}&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&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;	-H&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Content-Type: application&#x2F;json&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If you have used the correct IDs (both for request ID and user ID), you can now verify the balances using the pre-defined view &lt;code&gt;vacation_balances&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;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; curl &amp;quot;http:&#x2F;&#x2F;localhost:3000&#x2F;vacation_balances&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This demonstrates the basic workflow and how it can be exposed using PostgREST as an API. If you want to continue practising more database logic, you can add the following logic:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Prevent negative vacation balances (intermediate)&lt;&#x2F;li&gt;
&lt;li&gt;Prevent employees of a single manager from requesting overlapping vacations (advanced)&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;end-of-part-2&quot;&gt;End of Part 2&lt;a class=&quot;zola-anchor&quot; href=&quot;#end-of-part-2&quot; aria-label=&quot;Anchor link for: end-of-part-2&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;By introducing a dedicated API schema, we have added an abstraction layer that provides an effective boundary between the model&#x2F;logic itself and the API specifics. While this is a good practice, it&#x27;s up to you how to expose functionality. The main benefit of a dedicated schema is simplicity, allowing you to grant privileges on the schema in bulk rather than maintaining granular permissions. It also provides an easy way to expose existing databases with PostgREST.&lt;&#x2F;p&gt;
&lt;p&gt;While the current state of the sample API for Time Off Manager would work for small teams, in the next part, we will move towards authentication for added security and privacy.&lt;&#x2F;p&gt;
&lt;p&gt;And don&#x27;t forget you can find the source code in the &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;boringSQL&#x2F;postgrest-tutorial&quot;&gt;GitHub repository&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Deep Dive into PostgREST - Time Off Manager (Part 1)</title>
        <published>2024-05-11T00:00:00+00:00</published>
        <updated>2024-05-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/postgrest-tutorial-part1/"/>
        <id>https://boringsql.com/posts/postgrest-tutorial-part1/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/postgrest-tutorial-part1/">&lt;p&gt;The primary motivation behind &lt;strong&gt;boringSQL&lt;&#x2F;strong&gt; is to explore the robust world of SQL and the PostgreSQL ecosystem, demonstrating how these &quot;boring&quot; tools can cut through the ever-increasing noise and complexity of modern software development. In this series, I&#x27;ll guide you through building a simple yet fully functional application—a Time Off Manager. The goal of this project is not only to demonstrate practical database&#x2F;SQL approaches but also to provide a complete, extendable solution that you can immediately build upon. Each part of this series will deliver a self-contained application, setting the stage for introducing more complex functionalities and practices.&lt;&#x2F;p&gt;
&lt;p&gt;The first part of this guide focuses mainly on the application logic and exposing raw data using &lt;strong&gt;postgREST&lt;&#x2F;strong&gt;, allowing you to grasp the concept.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;requirements&quot;&gt;Requirements&lt;a class=&quot;zola-anchor&quot; href=&quot;#requirements&quot; aria-label=&quot;Anchor link for: requirements&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;This tutorial assumes you can install PostgreSQL, connect to it using your preferred DB client, have permissions to create a new database, and understand the basics of schema operations and SQL. Similarly, you should be able to follow the installation instructions for &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;postgrest.org&#x2F;en&#x2F;v12&#x2F;explanations&#x2F;install.html&quot;&gt;postgREST&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;The complete source code for the guide is available at &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;boringSQL&#x2F;postgrest-tutorial&quot;&gt;GitHub&lt;&#x2F;a&gt;.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-time-off-manager&quot;&gt;The Time Off Manager&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-time-off-manager&quot; aria-label=&quot;Anchor link for: the-time-off-manager&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;When choosing a sample application for this tutorial, I was torn between the popular but simple TodoMVC and a slightly more complex application. In the end, a sample like Time Off Manager provides a much richer database scheme, going beyond the basics and offering a solution closer to real-life systems.&lt;&#x2F;p&gt;
&lt;p&gt;Time Off Manager is also an excellent example that combines simple business logic, workflows, and practicality. The main requirements are:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Maintaining the list of users (employees) with their respective managers&lt;&#x2F;li&gt;
&lt;li&gt;Allowing the maintenance of the time off balance, with an audit history&lt;&#x2F;li&gt;
&lt;li&gt;Introducing an approval workflow and automation&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;While the application is intended for learning purposes, the database and API can be easily exposed and built upon.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-database&quot;&gt;The Database&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-database&quot; aria-label=&quot;Anchor link for: the-database&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;To get started, you will need to create a database. You can do this in two ways, either using the CREATE DATABASE statement:&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 time_off_manager;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;users-model&quot;&gt;Users model&lt;a class=&quot;zola-anchor&quot; href=&quot;#users-model&quot; aria-label=&quot;Anchor link for: users-model&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The foundation and first sample functionality exposed by postgREST is the users themselves. The table schema behind it represents the employees and manages hierarchical relations, which will become important in the second part when we set up the approval workflow.&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; users&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;    user_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;int GENERATED BY DEFAULT 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;    email &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;text NOT NULL UNIQUE&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;    manager_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;int REFERENCES&lt;&#x2F;span&gt;&lt;span&gt; users(user_id),&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;timestamp with time zone DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; current_timestamp,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    deleted_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamp with time zone&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;For testing purposes, let&#x27;s populate the data using a seed of 3 managers (one being &quot;the boss&quot; and hence not having a manager) and 10 employees.&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;DO $$&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DECLARE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    v_boss_id &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;    v_manager_id &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;    i &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 style=&quot;color: #F97583;&quot;&gt;BEGIN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    -- create &amp;quot;the boss&amp;quot;&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; users (email, manager_id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;owner@example.com&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;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;    RETURNING user_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INTO&lt;&#x2F;span&gt;&lt;span&gt; v_boss_id;&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: #6A737D;&quot;&gt;    -- setup 2 managers &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    FOR&lt;&#x2F;span&gt;&lt;span&gt; i &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN&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;2&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; LOOP&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; users (email, manager_id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;manager&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&lt;&#x2F;span&gt;&lt;span&gt; i &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;@example.com&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, v_boss_id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        RETURNING user_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INTO&lt;&#x2F;span&gt;&lt;span&gt; v_manager_id;&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;        -- and 5 employees for each one of them&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        FOR&lt;&#x2F;span&gt;&lt;span&gt; j &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN&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;5&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; LOOP&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; users (email, manager_id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;            VALUES&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;employee&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ||&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 style=&quot;color: #F97583;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span&gt; (i &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;+&lt;&#x2F;span&gt;&lt;span&gt; j) &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;@example.com&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, v_manager_id);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        END LOOP&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;    END LOOP&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;END&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;In case you are not aware, the seed data is produced by an &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;docs&#x2F;current&#x2F;sql-do.html&quot;&gt;anonymous block&lt;&#x2F;a&gt;. This way, we executed the logic to create the relationship required without the need to create (and later drop) the &lt;em&gt;PL&#x2F;pgSQL&lt;&#x2F;em&gt; function.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;expose-users-using-postgrest&quot;&gt;Expose users using postgREST&lt;a class=&quot;zola-anchor&quot; href=&quot;#expose-users-using-postgrest&quot; aria-label=&quot;Anchor link for: expose-users-using-postgrest&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Having the users table in place, together with sample data, we are ready to expose the data using the REST API. To start, you only need to create a very simple configuration for &lt;strong&gt;postgREST&lt;&#x2F;strong&gt; using the file postgrest.conf (you can place the file in the folder created for this sample project).&lt;&#x2F;p&gt;
&lt;p&gt;Sample &lt;code&gt;postgrest.conf&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;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;db-uri = &amp;quot;postgres:&#x2F;&#x2F;username:password@localhost&#x2F;time_off_manager&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;db-schema = &amp;quot;public&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;db-anon-role = &amp;quot;time_off_anonymous&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The most important part is the db-uri, which follows the standard PostgreSQL connection string format:&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;postgres:&#x2F;&#x2F;username:password@host&#x2F;database_name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;You need to replace:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;username&lt;&#x2F;code&gt; - with your actual database username. Ideally, you will use a separate user (for example, time_off_manager) for each application, rather than your personal account.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;password&lt;&#x2F;code&gt; - the password for the user&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;host&lt;&#x2F;code&gt; - either localhost (if running locally) or a remote server name or IP address&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;database_name&lt;&#x2F;code&gt; - in this case, time_off_manager we created earlier.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;em&gt;NOTE: We are using hardcoded data (which can potentially get stored in your source code repository), and this is mainly for learning purposes. Any deployment resembling a production environment should follow best practices to reduce security risks.&lt;&#x2F;em&gt;&lt;&#x2F;p&gt;
&lt;p&gt;The other configuration options are &lt;code&gt;db-schema&lt;&#x2F;code&gt;, which we will leave pointing to the public schema for this part, and &lt;code&gt;db-anon-role&lt;&#x2F;code&gt;, configuring the role postgREST should use when executing requests on behalf of unauthenticated clients (effectively all requests in Part 1).&lt;&#x2F;p&gt;
&lt;p&gt;For PostgreSQL 15 and higher, please make sure your &lt;code&gt;username&lt;&#x2F;code&gt; is the owner of the database created (should you create it alternatively) in order to be able to create objects in &lt;code&gt;public&lt;&#x2F;code&gt; schema.&lt;&#x2F;p&gt;
&lt;p&gt;To set up the database role, you need to run the following commands:&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 ROLE&lt;&#x2F;span&gt;&lt;span&gt; time_off_anonymous NOLOGIN;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GRANT SELECT&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INSERT&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;UPDATE ON&lt;&#x2F;span&gt;&lt;span&gt; users &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; time_off_anonymous;&lt;&#x2F;span&gt;&lt;&#x2F;span&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; time_off_anonymous &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; CURRENT_USER;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Please note you this example assumes &lt;code&gt;CURRENT_USER&lt;&#x2F;code&gt; is same as the username used in the &lt;code&gt;db-uri&lt;&#x2F;code&gt; configured above. The latter GRANT statement assigns the role time_off_anonymous to the configured user to execute the anonymous commands.&lt;&#x2F;p&gt;
&lt;p&gt;Once you have the configuration file ready (assuming it&#x27;s in the current directory), you can start the postgREST server and expose it locally on port 3000 (by default):&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;$ postgrest postgrest.con&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;10&#x2F;May&#x2F;2024:22:06:12 +0200: Starting PostgREST 12.0.3...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;10&#x2F;May&#x2F;2024:22:06:12 +0200: Attempting to connect to the database...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;10&#x2F;May&#x2F;2024:22:06:12 +0200: Connection successful&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;10&#x2F;May&#x2F;2024:22:06:12 +0200: Listening on port 3000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;10&#x2F;May&#x2F;2024:22:06:12 +0200: Listening for notifications on the pgrst channel&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;10&#x2F;May&#x2F;2024:22:06:12 +0200: Config reloaded&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;10&#x2F;May&#x2F;2024:22:06:12 +0200: Schema cache loaded&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;exploring-the-users-model-using-curl&quot;&gt;Exploring the Users Model Using cURL&lt;a class=&quot;zola-anchor&quot; href=&quot;#exploring-the-users-model-using-curl&quot; aria-label=&quot;Anchor link for: exploring-the-users-model-using-curl&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;When you have the server successfully running, you can try to access the data 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;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; curl &amp;quot;http:&#x2F;&#x2F;localhost:3000&#x2F;users&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;and get the list of all the sample data we created above. The API for reading data is &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;postgrest.org&#x2F;en&#x2F;v12&#x2F;references&#x2F;api&#x2F;tables_views.html#read&quot;&gt;described in detail&lt;&#x2F;a&gt; in the documentation, but you can try different alternatives.&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;# get one specific user&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; curl http:&#x2F;&#x2F;localhost:3000&#x2F;users?user_id=eq.10&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# query only active (i.e. non-deleted users)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;curl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;http:&#x2F;&#x2F;localhost:3000&#x2F;users?deleted_at=is.null&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# or display users without any manager (i.e. boss)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;curl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;http:&#x2F;&#x2F;localhost:3000&#x2F;users?manager_id=is.null&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Similarly, you can create, update, and delete users as required—giving you full CRUD functionality with rich filtering options.&lt;&#x2F;p&gt;
&lt;p&gt;Create user:&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;$ curl -X POST &amp;quot;http:&#x2F;&#x2F;localhost:3000&#x2F;users&amp;quot; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       -H &amp;quot;Content-Type: application&#x2F;json&amp;quot; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       -d &amp;#39;{&amp;quot;email&amp;quot;: &amp;quot;admin1@example.com&amp;quot;, &amp;quot;manager_id&amp;quot;: 1}&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Update user:&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;$ curl -X PATCH &amp;quot;http:&#x2F;&#x2F;localhost:3000&#x2F;users?user_id=eq.10&amp;quot; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       -H &amp;quot;Content-Type: application&#x2F;json&amp;quot; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       -d &amp;#39;{&amp;quot;email&amp;quot;: &amp;quot;updateduser@example.com&amp;quot;}&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;or, delete one:&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;$ curl -X DELETE &amp;quot;http:&#x2F;&#x2F;localhost:3000&#x2F;users?user_id=eq.10&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;I haven&#x27;t mentioned CRUD by accident; that&#x27;s the primary use-case of postgREST—exposing CRUD operations on the tables within the configured schema (&lt;code&gt;public&lt;&#x2F;code&gt; in this part). You can delegate authorisation at the database level.&lt;&#x2F;p&gt;
&lt;p&gt;Please note, the IDs are hardcoded, and you might need to check the output of the commands to get the correct ones (should you have truncated the table before or otherwise manipulated the data).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;basic-models-for-managing-time-off&quot;&gt;Basic models for managing time off&lt;a class=&quot;zola-anchor&quot; href=&quot;#basic-models-for-managing-time-off&quot; aria-label=&quot;Anchor link for: basic-models-for-managing-time-off&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;With the User model set up and tested via the API, it&#x27;s time to return to the core of the Time Off Manager functionality: absence tracking itself. For these purposes, we will define two tables—leave types (identifying the reason or type of the leave) and the time off transactions (or updates) themselves.&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; leave_types&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;    leave_type_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;int GENERATED BY DEFAULT 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;    label &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;text 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;    description 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&gt;    max_days &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;    created_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamp with time zone DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; current_timestamp,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    deleted_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamp with time zone&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;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; time_off_transactions&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;    transaction_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;int GENERATED BY DEFAULT 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;    created_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamp with time zone DEFAULT&lt;&#x2F;span&gt;&lt;span&gt; current_timestamp,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    user_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;int NOT NULL REFERENCES&lt;&#x2F;span&gt;&lt;span&gt; users(user_id),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    leave_type_id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;int NOT NULL REFERENCES&lt;&#x2F;span&gt;&lt;span&gt; leave_types(leave_type_id),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    transaction_date &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;date&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;    time_off_period daterange,&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;int 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;    description 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: #F97583;&quot;&gt;CREATE INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; transaction_for_user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; time_off_transactions(user_id);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The tables should be self-explanatory, but for clarity, let&#x27;s delve into the detail of the transaction tracking. Time off is tracked for individual users, the time off transaction type to identify the reason for the change, the period during which the absence is recorded, and the amount of effective days (simplified logic to account for weekends, public holidays, and similar).&lt;&#x2F;p&gt;
&lt;p&gt;To get started we also need to seed the 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;INSERT INTO&lt;&#x2F;span&gt;&lt;span&gt; leave_types (label, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;description&lt;&#x2F;span&gt;&lt;span&gt;, max_days) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VALUES&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: #9ECBFF;&quot;&gt;&amp;#39;vacation&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Annual vacation leave&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;25&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: #9ECBFF;&quot;&gt;&amp;#39;sick-leave&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Leave for health reasons&amp;#39;&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;unpaid-leave&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Leave without pay&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;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;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;sabbatical&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Extended leave for study or travel&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;NULL&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 create the initial time off balances (this is a simplified version, assuming the employee always has the full balance, ignoring possible mid-year joiners, etc.):&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;DO $$&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DECLARE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    v_leave_type record;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BEGIN&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    FOR&lt;&#x2F;span&gt;&lt;span&gt; v_leave_type &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;IN SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; leave_types &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; label &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;vacation&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; LOOP&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; time_off_transactions (user_id, leave_type_id, transaction_date, amount, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;description&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;        SELECT&lt;&#x2F;span&gt;&lt;span&gt; user_id, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;v_leave_type&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;leave_type_id&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&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: #79B8FF;&quot;&gt;v_leave_type&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;max_days&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Initial balance for year 2024&amp;#39;&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; users;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    END LOOP&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;END&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;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This now gives us the ability to start tracking the leave of absence for users and keep a full audit log of it.&lt;&#x2F;p&gt;
&lt;p&gt;Before we can start using the table via the API, we need to complete the last important step: giving access to the configured db-anon-role for the tables. We can do this by assigning the grant to individual tables 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;GRANT SELECT&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INSERT&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;UPDATE ON&lt;&#x2F;span&gt;&lt;span&gt; leave_types &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; time_off_anonymous;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GRANT SELECT&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;INSERT&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;UPDATE ON&lt;&#x2F;span&gt;&lt;span&gt; time_off_transactions &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;span&gt; time_off_anonymous;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;or modify the default privileges. Spoiler alert—we will move away from using the public schema in Part 2, so let&#x27;s not do more than we need at the moment :)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;working-with-absence&quot;&gt;Working with absence&lt;a class=&quot;zola-anchor&quot; href=&quot;#working-with-absence&quot; aria-label=&quot;Anchor link for: working-with-absence&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Similar to the users example, we can now track absence directly using the API. Before firing off cURL commands, there&#x27;s one last thing to remember. &lt;strong&gt;postgREST&lt;&#x2F;strong&gt; requires metadata about the database schema itself, and it might be expensive to perform on the fly, hence to avoid this, the server caches the schema.&lt;&#x2F;p&gt;
&lt;p&gt;To reload the schema (after making schema changes), it is required to reload the cache. This can be done via several basic options:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;restarting the server&lt;&#x2F;li&gt;
&lt;li&gt;issuing a (SIGUSR1) signal to the server process&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;and &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;postgrest.org&#x2F;en&#x2F;v12&#x2F;references&#x2F;schema_cache.html#automatic-schema-cache-reloading&quot;&gt;more advanced ways&lt;&#x2F;a&gt; (outside Part 1 of this tutorial).&lt;&#x2F;p&gt;
&lt;p&gt;Once reloaded, you can start exploring more:&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;# getting a time off transaction for particular user&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ curl &amp;quot;http:&#x2F;&#x2F;localhost:3000&#x2F;time_off_transactions?user_id=eq.1&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# submitting new leave of absence&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ curl -X POST http:&#x2F;&#x2F;localhost:3000&#x2F;time_off_transactions \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       -H &amp;quot;Content-Type: application&#x2F;json&amp;quot; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       -d &amp;#39;{&amp;quot;user_id&amp;quot;: 5, &amp;quot;leave_type_id&amp;quot;: 1, &amp;quot;transaction_date&amp;quot;: &amp;quot;2024-02-26&amp;quot;, &amp;quot;time_off_period&amp;quot;: &amp;quot;[2024-02-28,2024-03-04]&amp;quot;, &amp;quot;amount&amp;quot;: -4, &amp;quot;description&amp;quot;: &amp;quot;Vacation to avoid panic over leap year&amp;quot;}&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;All these commands work as expected. Should you forget to reload the schema, you might face an HTTP Status 404 (Not Found) on the newly created objects.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;end-of-part-1&quot;&gt;End of Part 1&lt;a class=&quot;zola-anchor&quot; href=&quot;#end-of-part-1&quot; aria-label=&quot;Anchor link for: end-of-part-1&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;While not being very sophisticated, I hope this first part has demonstrated the CRUD capabilities &lt;strong&gt;postgREST&lt;&#x2F;strong&gt; can offer with minimal effort. In the next part, we will move away from accessing the database tables directly and provide more functionality via a new schema api, providing a per-user time off balance view and introducing workflow management.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>How not to change PostgreSQL column type</title>
        <published>2024-05-04T00:00:00+00:00</published>
        <updated>2024-05-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/how-not-to-change-postgresql-column-type/"/>
        <id>https://boringsql.com/posts/how-not-to-change-postgresql-column-type/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/how-not-to-change-postgresql-column-type/">&lt;p&gt;One of the surprises that comes with developing applications and operating a database cluster behind them is the discrepancy between practice and theory, development environment and the production. A perfect example of such a mismatch is changing a column type.&lt;&#x2F;p&gt;
&lt;p&gt;The conventional knowledge on how to change a column type in PostgreSQL (and other systems compliant with the SQL standard) is to:&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 TABLE&lt;&#x2F;span&gt;&lt;span&gt; table_name&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&lt;&#x2F;span&gt;&lt;span&gt; COLUMN column_name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[SET DATA]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; TYPE&lt;&#x2F;span&gt;&lt;span&gt; new_data_type&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;which is obviously the semantically correct way, but given the right circumstances, you might be set for a rather unpleasant surprise.&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;Let&#x27;s create a sample table and demonstrate the isolated behaviour that you might observe. Let&#x27;s start with 10 million rows (which really is just a drop in the whole world 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;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- create very simple table&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 TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; sample_table&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;INT 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;    label &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&gt;    created_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW&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;    updated_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- populate with 10m records&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; sample_table (label)&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 style=&quot;color: #9ECBFF;&quot;&gt;  &amp;#39;hash: &amp;#39;&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;7000000&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 let&#x27;s change the id type from INT to BIGINT.&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;alter_type_demo=# ALTER TABLE sample_table ALTER COLUMN id TYPE bigint;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ALTER TABLE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Time: 21592.190 ms (00:21.592)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And ... 21 seconds later, you have your change. Mind you, this is a small table with roughly 600 MB of data in it. What if you going to face 100x that amount? Let&#x27;s have a look what went on behind the scene.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-things-postgresql-must-do&quot;&gt;The things PostgreSQL must do&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-things-postgresql-must-do&quot; aria-label=&quot;Anchor link for: the-things-postgresql-must-do&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;Changing a data type (and many other operations you might experience) is no simple task, and the PostgreSQL engine has to perform several tasks:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rewrite the table&lt;&#x2F;strong&gt; is the most obvious culprit. Changing a column from INT to BIGINT requires 4 additional bytes to be allocated for every single tuple (ahem, think row). As the original table schema required a fixed amount of bytes, the cluster stored them in the most efficient way. i.e., every single row, in our example 10 million of them, must be read and re-written using the correct tuple size.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Locks&lt;&#x2F;strong&gt; might not be a problem in our synthetic example, but should you execute the ALTER command in production with hundreds or thousands of concurrent queries, you will have to wait for all of them to release the locks.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Indexes and constraints&lt;&#x2F;strong&gt; - if the column being altered is indexed or has constraints, they need to be rebuilt&#x2F;revalidated. This is additional overhead.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Transactions &amp;amp; Write-Ahead Log&lt;&#x2F;strong&gt; is another big part of the problem. To guarantee durability (the &#x27;D&#x27; in ACID), PostgreSQL has to record each change in WAL files. This way, if the database crashes, the system can replay the WAL files to reconstruct the lost modifications since the last checkpoint.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;As you can see, there&#x27;s quite a bit involved in performing something that might be understood as routine table maintenance. The size of the data being modified, disk I&#x2F;O and capacity, and general system congestion come into play.&lt;&#x2F;p&gt;
&lt;p&gt;But the real problem does not end here. If we are talking about any sort of serious production deployment, you must consider more things:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Real-time &lt;strong&gt;replication&lt;&#x2F;strong&gt;, both physical and logical: This adds an additional layer of complexity. For read replicas, the default behaviour ensures that a synchronous commit is maintained to achieve consistency across the database cluster. This setup guarantees that a transaction is only finalised once all standby replicas have confirmed receipt of the changes. However, this introduces new challenges, as the performance now also depends on network throughput—including potential congestion—and the latency and I&#x2F;O performance of the standby nodes.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Recovery and backups&lt;&#x2F;strong&gt; are another important area to consider. While the regular backups&#x27; size might not be affected that much, you have to consider everything that happens between the last backup before the change and the next one and ensure point-in-time consistency.&lt;&#x2F;li&gt;
&lt;li&gt;Less common but not unheard of might be &lt;strong&gt;asynchronous replicas&lt;&#x2F;strong&gt; or reserved slots for &lt;strong&gt;logical replication&lt;&#x2F;strong&gt;. Generating a large volume of changes (and therefore WAL files) can leave the less performant (or infrequent) replication systems behind for a considerable amount of time. While this might be acceptable, you need to make sure the source system has enough disk space to hold the WAL files for a long enough time&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;As you can see, altering the column data type is not as straightforward as it might seem. Current CI&#x2F;CD practices often make it very easy for software developers to commit and roll out database migrations to a production environment, only to find themselves in the middle of a production incident minutes later. While a staging deployment might help, it&#x27;s not guaranteed to share the same characteristics as production (either due to the level of load or monetary constraints).&lt;&#x2F;p&gt;
&lt;p&gt;The problem is, therefore (and I will repeat myself), the scale of the amount of data being modified, overall congestion of the system, I&#x2F;O capacity, and the target table&#x27;s importance in the application design.&lt;&#x2F;p&gt;
&lt;p&gt;At the end of the day, it translates to the &lt;strong&gt;total time&lt;&#x2F;strong&gt; needed for the migration to finish, and the unique constraints your business can or maybe cannot afford. The simplest solution to the problem is to schedule the planned maintenance during low traffic periods and get it done.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-to-safely-change-a-postgresql-column-type&quot;&gt;&lt;strong&gt;How to Safely Change a PostgreSQL Column Type&lt;&#x2F;strong&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#how-to-safely-change-a-postgresql-column-type&quot; aria-label=&quot;Anchor link for: how-to-safely-change-a-postgresql-column-type&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;What if you need to rewrite hundreds of gigabytes or even terabytes of data and can&#x27;t afford anything more than minimal downtime? Let&#x27;s explore how to change a column type properly.&lt;&#x2F;p&gt;
&lt;p&gt;Let&#x27;s start with &lt;strong&gt;The Bad News&lt;&#x2F;strong&gt; - you cannot avoid rewriting the entire table, which will generate a significant amount of WAL files in the process. This is a given, and you must plan how to manage it.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;The Good News:&lt;&#x2F;strong&gt; You can spread the potential downtime over a much longer period than it might take to process the data. The specific requirements and constraints will vary based on individual business needs, so careful planning is essential.&lt;&#x2F;p&gt;
&lt;p&gt;The full migration can be summarised as series of following steps:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Add a new column&lt;&#x2F;strong&gt; to the target table with the correct type. Ensure the column is NULLable and does not have a default value to avoid forcing a full table rewrite[^1]. For example, should you need to increase ID of &lt;code&gt;order_id&lt;&#x2F;code&gt; you will end up with new column &lt;code&gt;new_order_id&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Setup a trigger&lt;&#x2F;strong&gt; to update the new column as new data comes in. This ensures that all new data during the migration will have the new column populated.&lt;&#x2F;li&gt;
&lt;li&gt;Implement a function or logic to &lt;strong&gt;batch migrate&lt;&#x2F;strong&gt; the values from old to new column over time. The size of the batch and the timing should align with the operational constraints of your business&#x2F;environment.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Migrate Old Values:&lt;&#x2F;strong&gt; Depending on your constraints, data size, and I&#x2F;O capabilities, this process could take anywhere from hours to several weeks, or possibly longer. While a SQL or PL&#x2F;pgSQL function running in a terminal session (consider using tmux) might suffice for shorter migrations, more extended migrations may require a more sophisticated approach. This topic alone could be a good subject for a separate blog post or guide.&lt;&#x2F;li&gt;
&lt;li&gt;Once the migration is complete, &lt;strong&gt;create constraints and indexes&lt;&#x2F;strong&gt; that reflect the new column. Be aware of potential locking issues, especially if the field is part of any foreign keys.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;At this point you are ready to perform the switch itself. If you can verify all rows have correctly populated new column, it&#x27;s time to embrace the most difficult part. If possible in one transaction and or smaller scheduled downtime&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Drop the legacy column&lt;&#x2F;strong&gt;. This operation usually only locks the table for a short duration.&lt;&#x2F;li&gt;
&lt;li&gt;After dropping the old column, &lt;strong&gt;rename the new column&lt;&#x2F;strong&gt;. This step completes most of the migration process.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;It&#x27;s good practise to consider the restart of all the application relying on the changed table, as some tools (ORMs... I&#x27;m looking at you) might cache the OIDs and not handle the change gracefully.&lt;&#x2F;p&gt;
&lt;p&gt;And that would be it - except not really. Dropping the column only removes the reference and the data itself will remain physically on the disk. This is the scenario where you will might need to perform &lt;code&gt;VACUUM FULL&lt;&#x2F;code&gt; - which could lock the table and rewrite it completely—potentially defeating the purpose of a concurrent migration. This brings us back to the original article which motivated me to write this guide - [[The Bloat Busters: pg_repack vs pg_squeeze]] is the way to go. Preparation and familiarity with these tools in advance are highly recommended.&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;While changing the column type in PostgreSQL can be as simple as issuing an ALTER TABLE command, it is important for everyone involved to understand the complexities attached to it. Whether you are the software developer requesting the change, someone reviewing it, or the individual tasked with resolving incidents when such changes are deployed to the production environment without careful planning, a deep understanding of this process is crucial. Moreover, grasping this particular change enables you to easily project insights onto other potentially costly operations.&lt;&#x2F;p&gt;
&lt;p&gt;[^1] Correction: it&#x27;s actually done without full table rewrite since PostgreSQL 11 (&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;brandur.org&#x2F;postgres-default&quot;&gt;Fast Column Creation with Defaults&lt;&#x2F;a&gt;)&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>
    <entry xml:lang="en">
        <title>Are SQL &amp; Databases Boring? Absolutely - and That&#x27;s a Good Thing!</title>
        <published>2024-04-21T00:00:00+00:00</published>
        <updated>2024-04-21T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/boring-sql-and-databases/"/>
        <id>https://boringsql.com/posts/boring-sql-and-databases/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/boring-sql-and-databases/">&lt;p&gt;Twenty years ago, I would have laughed if you had told me I&#x27;d be promoting databases as the technology to turn to. Back then, databases were just a &#x27;dummy&#x27; storage for me—a necessary evil, a sentiment shared by many developers. At that point, I felt so strongly about it that I was a trainer for Hibernate, a tool which helped me keep those pesky databases at arm&#x27;s length.&lt;&#x2F;p&gt;
&lt;p&gt;Fast forward, and here I am, starting &lt;strong&gt;boringSQL&lt;&#x2F;strong&gt;, a site dedicated to helping demystify the depths of SQL and databases in general. Whether you&#x27;ve just started or want to gain expert knowledge. What changed, you might ask? Nothing actually. Well, a few things did. The frameworks changed. I moved from the Java world to Rails, jumped to Python and then to Go, worked with countless frontend paradigms, and oversaw the implementation of a number of technologies. The same experiments were tried again and again. Only one part of the tech stood the test of time. Yes, it&#x27;s the database. And out of many, one in particular—PostgreSQL. And while alternatives promised, and still promise, that the grass would be greener—I can honestly say that whatever revolution comes in the technology landscape, databases will hold the same place in the next twenty years.&lt;&#x2F;p&gt;
&lt;p&gt;This is not the first &lt;strong&gt;boring&lt;&#x2F;strong&gt; site, and rest assured, the concept of celebrating the mundane in technology is far from new. The tech landscape is paved with many &quot;boring&quot; things, from frameworks to methodologies to complete stacks. Businesses depend on these technologies to run their operations day in, day out. And while it might be fun to try new things every month, it&#x27;s not the headline-driven development that is the way forward.&lt;&#x2F;p&gt;
&lt;p&gt;Hence, if you ask—is SQL boring? Are databases boring? I would also say YES. They are among the finalists of the least popular conversation starters. But it&#x27;s the quiet power of reliability that has a healing effect in a world where technology changes at the speed of light.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>When and Why PostgreSQL Indexes Are Ignored</title>
        <published>2024-04-14T00:00:00+00:00</published>
        <updated>2024-04-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/why-postgresql-indexes-are-ignored/"/>
        <id>https://boringsql.com/posts/why-postgresql-indexes-are-ignored/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/why-postgresql-indexes-are-ignored/">&lt;p&gt;While it&#x27;s true the most problems can be solved by the appropriate use of the index, there are cases where you will just waste resources doing so. For casual developer it might seems like PostgreSQL decided to do its own thing, but when you look behind the scenes it all makes perfect sense. Here&#x27;s a quick run down of the some reasons why planner might pass on index and rather do things without it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-case-of-missing-condition&quot;&gt;The case of missing condition&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-case-of-missing-condition&quot; aria-label=&quot;Anchor link for: the-case-of-missing-condition&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;In the evolution of the software developer journey into the realms of databases, the partial indexes are the next best thing. Why to create the full index where you can easily restrict it to the sub-set of the records? The trick is ensure every query aligns with the condition given during the index creation.&lt;&#x2F;p&gt;
&lt;p&gt;Think of an order management system, where most active orders are going to those in &#x27;open&#x27; status. Most of the day-to-day operational tasks will resolve around those entries, a partial index like&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 INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; open_orders&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; orders(order_id) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE state =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;open&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;provides a perfect match. It will allow to iterate only on a relatively small subset of the data, hence saving the disc space. As long as the application developers are aware of the partial condition, and how to use it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-column-order-matters&quot;&gt;The column order matters&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-column-order-matters&quot; aria-label=&quot;Anchor link for: the-column-order-matters&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The second common case is just a step away from the partial index. The compound indexes, or multi-column indexes, are versatile and powerful - that is when used correctly. The crucial element is the order in which the columns are indexed. The order impacts whatever it gets used or not at all.&lt;&#x2F;p&gt;
&lt;p&gt;The key is to match the left-most columns of the index in your queries. Once these are aligned, you can optionally skip or include additional columns in the filter, but the initial columns must be used to leverage the index effectively.&lt;&#x2F;p&gt;
&lt;p&gt;While it might be easy not to miss the condition of the country, when filtering for cities&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 INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; contact_location&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; contact_details (country_id, city);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;consider the scenario which might not be so obvious.&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 INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; brand_products_per_category&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; products (category_id, brand_id);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In this case the index will only get used if either both &lt;code&gt;category_id&lt;&#x2F;code&gt; and &lt;code&gt;brand_id&lt;&#x2F;code&gt; are used, or at least &lt;code&gt;category_id&lt;&#x2F;code&gt; - but not for &lt;code&gt;brand_id&lt;&#x2F;code&gt; alone. In latter case PostgreSQL planner might (unless another index is available) to opt in for scan of the entire table.&lt;&#x2F;p&gt;
&lt;p&gt;In this case alternative strategy would be to switch the column order (should the application logic support it).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;low-selectivity&quot;&gt;Low Selectivity&lt;a class=&quot;zola-anchor&quot; href=&quot;#low-selectivity&quot; aria-label=&quot;Anchor link for: low-selectivity&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;If we use the example of the partial index from the first section, we can easily demonstrate another case when index just might get ignored forever. It&#x27;s the case for the columns with low selectivity, where a predominant value overshadows others, making an index less useful.&lt;&#x2F;p&gt;
&lt;p&gt;Take example of the ordering system. While having index for &lt;code&gt;open&lt;&#x2F;code&gt; orders is helpful, in most scenarios majority of the records will end up in &lt;code&gt;delivered&lt;&#x2F;code&gt; state. Hence&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 INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; delivered_orders&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; orders(order_id) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE state =&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;p&gt;might seem beneficial, but in reality PostgreSQL will most likely chose to skip it and instead to scan the table. The reason? Should you consider the regular business operations, you might find vast majority (let&#x27;s say 95%) or orders shipped and considered finished. With such a high percentage of the records orders, the index will do little to narrow the search for the records. Planner hence will perform the sequential scan directly. Why? It&#x27;s all to do with the statistics that are available on the table.&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;table_name        | orders&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;column_name       | state&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;n_distinct        | 4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;most_common_vals  | {delivered,cancelled,pending,open}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;most_common_freqs | {0.95023334,0.030166665,0.013,0.0096}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Those are fictional statistics matching the order distribution described above. From there you can see that &lt;code&gt;delivered&lt;&#x2F;code&gt; orders dominate the dataset, compromising approximately out of 95.02% of the records. The other statuses make up just for a small fraction.&lt;&#x2F;p&gt;
&lt;p&gt;The above data sample comes from the query similar to&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    tablename &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; table_name,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    attname &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; column_name,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    n_distinct,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    most_common_vals,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    most_common_freqs&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;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pg_stats&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    tablename &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;orders&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    AND&lt;&#x2F;span&gt;&lt;span&gt; schemaname &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;public&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;outdated-statistics&quot;&gt;&lt;strong&gt;Outdated Statistics&lt;&#x2F;strong&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#outdated-statistics&quot; aria-label=&quot;Anchor link for: outdated-statistics&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;As demonstrated low selectivity to addressed by statistics, instead of index, it&#x27;s not the only case where stale data can lead to inefficient query planning. Keeping database statistics current is crucial for PostgreSQL to make informed decisions about whether to use an index or opt for a sequential scan.&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL heavily relies on statistics to estimate costs and make the right decisions across different query execution plans. The statistics cover various data points, like total number of rows in table(s), distinct values, their distribution, histogram bounds, correlation between physical row ordering and column values, and much more.&lt;&#x2F;p&gt;
&lt;p&gt;The trouble starts when statistics get out-of-date. It&#x27;s similar as navigating using old maps. It just might you send you going in circles. How might the statistics in PostgreSQL get dated? Most cases involve &lt;strong&gt;high volume of data modifications&lt;&#x2F;strong&gt; or &lt;strong&gt;bulk operations&lt;&#x2F;strong&gt;, but will be affected also by schema changes. For all those operations it&#x27;s always good idea to &lt;code&gt;ANALYZE&lt;&#x2F;code&gt; the table to keep the DB up-to-date.&lt;&#x2F;p&gt;
&lt;p&gt;The prevent the statistics outdated, PostgreSQL alone either manual &lt;code&gt;ANALYZE&lt;&#x2F;code&gt; or periodically tries to trigger it as part of autovacuum daemon. The key factor is whatever it can run fast and frequently enough to keep up with the changes. You can adjust the autovaccuum settings globally or per-table.&lt;&#x2F;p&gt;
&lt;p&gt;Keeping statistics up-to-date is crucial for maintaining good query performance, as it ensures that the query planner has accurate information to base its decisions on.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;a class=&quot;zola-anchor&quot; href=&quot;#summary&quot; aria-label=&quot;Anchor link for: summary&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;As demonstrated creating index might not be always the best course of action. Understanding those edge-cases is crucial not only for DBAs but also for developers. By keeping those considerations you can better design the schema and indexing strategy.&lt;&#x2F;p&gt;
&lt;p&gt;Indexes are still the next best thing in database world, but they require thoughtful implementation and maintenance. The goal is not only to create indexes, but to &lt;strong&gt;create the right indexes&lt;&#x2F;strong&gt; based on accurate, up-to-date data and aligned with your specific query patterns.&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
