<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>Data Bene - Tag: 'Programming'</title>
  <subtitle>Relational database, open-source and scalable.</subtitle>
  <link href="https://www.data-bene.io/en/blog/tags/programming.xml" rel="self" type="application/atom+xml" />
  <updated>2025-11-13T00:00:00Z</updated>
  <id>https://www.data-bene.io/en/blog/tags/programming.xml</id>
    <entry>
      <title>Did you know? Tables in PostgreSQL are limited to 1,600 columns</title>
      <link href="https://www.data-bene.io/en/blog/did-you-know-tables-in-postgresql-are-limited-to-1600-columns/" />
      <updated>2025-11-13T00:00:00Z</updated>
      <id>https://www.data-bene.io/en/blog/did-you-know-tables-in-postgresql-are-limited-to-1600-columns/</id>
     <content type="html"><![CDATA[ <p><strong>Did you know a table can have no more than 1,600 columns?</strong> This blog article was inspired by a conversation Pierre Ducroquet and I had.</p>
<h2 id="first-the-documentation"><a class="heading-anchor" href="#first-the-documentation">First, the documentation</a></h2>
<p>The PostgreSQL documentation <a href="https://www.postgresql.org/docs/current/limits.html" rel="noopener">Appendix K</a> states a table can have a maximum of 1,600 columns.</p>
<p>This is a <strong>hard coded limit</strong> that can be found in the source code at <code>src/include/access/htup_details.h</code>:</p>
<pre class="language-plaintext"><code class="language-plaintext">#define MaxTupleAttributeNumber 1664
#define MaxHeapAttributeNumber	1600</code></pre>
<h2 id="reaching-the-limit-the-expected-way"><a class="heading-anchor" href="#reaching-the-limit-the-expected-way">Reaching the limit the expected way</a></h2>
<p>Let’s fully validate the claim and test accordingly.</p>
<h3 id="playing-with-table-definition"><a class="heading-anchor" href="#playing-with-table-definition">Playing with table definition</a></h3>
<p>Here, we’ll use a simple bash script because it is easy to adapt while testing.</p>
<pre class="language-sql"><code class="language-sql"><span class="token comment">-- Classic example</span>

<span class="token keyword">DO</span> $$             
<span class="token keyword">DECLARE</span>
    i <span class="token keyword">int</span><span class="token punctuation">;</span>
<span class="token keyword">BEGIN</span>
    <span class="token keyword">EXECUTE</span> <span class="token string">'DROP TABLE IF EXISTS tint_1601;'</span><span class="token punctuation">;</span>
    <span class="token keyword">EXECUTE</span> <span class="token string">'CREATE TABLE tint_1601(i_1 int);'</span><span class="token punctuation">;</span>
    <span class="token keyword">FOR</span> i <span class="token operator">IN</span> <span class="token number">2.</span><span class="token number">.1601</span> <span class="token keyword">LOOP</span>
        <span class="token keyword">EXECUTE</span> <span class="token function">format</span><span class="token punctuation">(</span><span class="token string">'ALTER TABLE tint_1601 ADD COLUMN i_%s int;'</span><span class="token punctuation">,</span> i<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">END</span> <span class="token keyword">LOOP</span><span class="token punctuation">;</span>
<span class="token keyword">END</span> $$<span class="token punctuation">;</span></code></pre>
<p>The typical output is as follows:</p>
<pre class="language-plaintext"><code class="language-plaintext">NOTICE:  table "tint_1600" does not exist, skipping
ERROR:  tables can have at most 1600 columns
CONTEXT:  SQL statement "ALTER TABLE tint_1600 ADD COLUMN i_1601 int;"
PL/pgSQL function inline_code_block line 8 at EXECUTE</code></pre>
<p>So far so good (or at least, all is working as expected).</p>
<p>You might have the idea to try replacing <code>int4</code> with <code>int2</code> type to create a 1,600+ column table. It will not work as this is a hard coded limit.</p>
<h3 id="playing-with-table-content"><a class="heading-anchor" href="#playing-with-table-content">Playing with table content</a></h3>
<p>Let’s build a 1,600 column table with the same demonstrated code.</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">DO</span> $$             
<span class="token keyword">DECLARE</span>
    i <span class="token keyword">int</span><span class="token punctuation">;</span>
<span class="token keyword">BEGIN</span>
    <span class="token keyword">EXECUTE</span> <span class="token string">'DROP TABLE IF EXISTS tint_1600;'</span><span class="token punctuation">;</span>
    <span class="token keyword">EXECUTE</span> <span class="token string">'CREATE TABLE tint_1600(i_1 int);'</span><span class="token punctuation">;</span>
    <span class="token keyword">FOR</span> i <span class="token operator">IN</span> <span class="token number">2.</span><span class="token number">.1600</span> <span class="token keyword">LOOP</span>
        <span class="token keyword">EXECUTE</span> <span class="token function">format</span><span class="token punctuation">(</span><span class="token string">'ALTER TABLE tint_1600 ADD COLUMN i_%s int;'</span><span class="token punctuation">,</span> i<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">END</span> <span class="token keyword">LOOP</span><span class="token punctuation">;</span>
<span class="token keyword">END</span> $$<span class="token punctuation">;</span></code></pre>
<p>Another sql script can be used to produce a valid 1,600 column tuple:</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">DO</span> $$
<span class="token keyword">DECLARE</span>
    s <span class="token keyword">TEXT</span><span class="token punctuation">;</span>
    rows_inserted <span class="token keyword">int</span><span class="token punctuation">;</span>
<span class="token keyword">BEGIN</span>
    s :<span class="token operator">=</span> <span class="token function">format</span><span class="token punctuation">(</span>
                 <span class="token string">'INSERT INTO tint_1600 VALUES (1%s);'</span>
               <span class="token punctuation">,</span> <span class="token keyword">repeat</span><span class="token punctuation">(</span> <span class="token string">',1'</span> <span class="token punctuation">,</span> <span class="token number">1599</span> <span class="token punctuation">)</span> 
               <span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">EXECUTE</span> s<span class="token punctuation">;</span>

    GET DIAGNOSTICS rows_inserted <span class="token operator">=</span> ROW_COUNT<span class="token punctuation">;</span>
    RAISE NOTICE <span class="token string">'Rows inserted: %'</span><span class="token punctuation">,</span> rows_inserted<span class="token punctuation">;</span>
<span class="token keyword">END</span> $$<span class="token punctuation">;</span></code></pre>
<p>The output is:</p>
<pre class="language-plaintext"><code class="language-plaintext">NOTICE:  Rows inserted: 1
DO</code></pre>
<p>Another success with no surprise.</p>
<h3 id="testing-the-limits"><a class="heading-anchor" href="#testing-the-limits">Testing the limits</a></h3>
<p>Let us continue pushing to the limits.</p>
<p>We now create another 1,600 column table using the <code>char(127)</code> data type.</p>
<p>We reuse our sql script with some modifications:</p>
<pre class="language-sql"><code class="language-sql"><span class="token comment">-- Create a table with 1,600 columns: 1 x int + 1599 x char(127)</span>
<span class="token keyword">DO</span> $$             
<span class="token keyword">DECLARE</span>
    i <span class="token keyword">int</span><span class="token punctuation">;</span>
<span class="token keyword">BEGIN</span>
    <span class="token keyword">EXECUTE</span> <span class="token string">'DROP TABLE IF EXISTS tint_1600;'</span><span class="token punctuation">;</span>
    <span class="token keyword">EXECUTE</span> <span class="token string">'CREATE TABLE tint_1600(i_1 int);'</span><span class="token punctuation">;</span>
    <span class="token keyword">FOR</span> i <span class="token operator">IN</span> <span class="token number">2.</span><span class="token number">.1600</span> <span class="token keyword">LOOP</span>
        <span class="token keyword">EXECUTE</span> <span class="token function">format</span><span class="token punctuation">(</span><span class="token string">'ALTER TABLE tint_1600 ADD COLUMN c_%s char(127) NOT NULL;'</span><span class="token punctuation">,</span> i<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">END</span> <span class="token keyword">LOOP</span><span class="token punctuation">;</span>
<span class="token keyword">END</span> $$<span class="token punctuation">;</span>

<span class="token comment">-- Insert a tuple - 1 x int + 1599 x char(127)</span>
<span class="token keyword">DO</span> $$
<span class="token keyword">DECLARE</span>
    s <span class="token keyword">TEXT</span><span class="token punctuation">;</span>
<span class="token keyword">BEGIN</span>
    s :<span class="token operator">=</span> <span class="token function">format</span><span class="token punctuation">(</span> 
                 <span class="token string">'INSERT INTO tint_1600 VALUES (1%s);'</span>
               <span class="token punctuation">,</span> <span class="token keyword">repeat</span><span class="token punctuation">(</span> $q$<span class="token punctuation">,</span><span class="token string">'1'</span>::<span class="token keyword">char</span><span class="token punctuation">(</span><span class="token number">127</span><span class="token punctuation">)</span>$q$ <span class="token punctuation">,</span> <span class="token number">1599</span> <span class="token punctuation">)</span> 
               <span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">EXECUTE</span> s<span class="token punctuation">;</span>
<span class="token keyword">END</span> $$<span class="token punctuation">;</span></code></pre>
<p>The output is:</p>
<pre class="language-plaintext"><code class="language-plaintext">ERROR:  row is too big: size 25616, maximum size 8160</code></pre>
<p>As we can see, the table has 1,600 columns but this time the tuple cannot fit a single heap page which explains the error “row is too big: size 25616, maximum size 8160”. If you paid attention to the modified script, you can see columns are defined as <code>NOT NULL</code> so at table creation PostgreSQL could have proven data insertion was impossible.</p>
<h2 id="what-about-joins"><a class="heading-anchor" href="#what-about-joins">What about JOINs?</a></h2>
<p>To keep things simple, let us auto-join:</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">SELECT</span> a<span class="token punctuation">.</span><span class="token operator">*</span><span class="token punctuation">,</span>b<span class="token punctuation">.</span><span class="token operator">*</span> <span class="token keyword">FROM</span> tint_1600 a<span class="token punctuation">,</span> tint_1600 b<span class="token punctuation">;</span>
ERROR:  target lists can have at most <span class="token number">1664</span> entries</code></pre>
<p>Now the <code>SELECT</code> clause (<code>a.*,b.*</code>) is reaching its own limit (<code>MaxTupleAttributeNumber = 1664</code>).</p>
<h2 id="reaching-the-column-limit-the-unexpected-way"><a class="heading-anchor" href="#reaching-the-column-limit-the-unexpected-way">Reaching the column limit the unexpected way</a></h2>
<p>Sometimes, you have to modify your application and it generates schema modifications.<br>
Most of the time, there are table modifications like adding or dropping columns.</p>
<h3 id="exploring-add-/-drop-column"><a class="heading-anchor" href="#exploring-add-/-drop-column">Exploring <code>ADD</code> / <code>DROP COLUMN</code></a></h3>
<p>Let us see what happens from the SQL side when we add, then drop, a column.</p>
<pre class="language-sql"><code class="language-sql"><span class="token operator">=</span><span class="token comment"># CREATE TABLE tadc_1600(i_1 int NOT NULL);</span>

<span class="token keyword">CREATE</span> <span class="token keyword">TABLE</span>

<span class="token operator">=</span><span class="token comment"># ALTER TABLE tadc_1600 ADD COLUMN i_2 int NOT NULL;</span>

<span class="token keyword">ALTER</span> <span class="token keyword">TABLE</span>

<span class="token operator">=</span><span class="token comment"># SELECT attname,attnum,attstorage,attnotnull,attisdropped </span>
   <span class="token keyword">FROM</span> pg_attribute 
   <span class="token keyword">WHERE</span> attrelid<span class="token operator">=</span><span class="token punctuation">(</span>
                   <span class="token keyword">SELECT</span> oid 
                   <span class="token keyword">FROM</span> pg_class 
                   <span class="token keyword">WHERE</span> relname<span class="token operator">=</span><span class="token string">'tadc_1600'</span>
                   <span class="token punctuation">)</span> 
     <span class="token operator">AND</span> attnum <span class="token operator">></span> <span class="token number">0</span> <span class="token keyword">ORDER</span> <span class="token keyword">BY</span> attnum<span class="token punctuation">;</span>
     
 attname <span class="token operator">|</span> attnum <span class="token operator">|</span> attstorage <span class="token operator">|</span> attnotnull <span class="token operator">|</span> attisdropped 
<span class="token comment">---------+--------+------------+------------+--------------</span>
 i_1     <span class="token operator">|</span>      <span class="token number">1</span> <span class="token operator">|</span> p          <span class="token operator">|</span> t          <span class="token operator">|</span> f
 i_2     <span class="token operator">|</span>      <span class="token number">2</span> <span class="token operator">|</span> p          <span class="token operator">|</span> t          <span class="token operator">|</span> f
<span class="token punctuation">(</span><span class="token number">2</span> <span class="token keyword">rows</span><span class="token punctuation">)</span>

<span class="token operator">=</span><span class="token comment"># ALTER TABLE tadc_1600 DROP COLUMN i_2;</span>

<span class="token keyword">ALTER</span> <span class="token keyword">TABLE</span>

<span class="token operator">=</span><span class="token comment"># SELECT attname,attnum,attstorage,attnotnull,attisdropped </span>
   <span class="token keyword">FROM</span> pg_attribute 
   <span class="token keyword">WHERE</span> attrelid<span class="token operator">=</span><span class="token punctuation">(</span>
                   <span class="token keyword">SELECT</span> oid 
                   <span class="token keyword">FROM</span> pg_class 
                   <span class="token keyword">WHERE</span> relname<span class="token operator">=</span><span class="token string">'tadc_1600'</span>
                   <span class="token punctuation">)</span> 
     <span class="token operator">AND</span> attnum <span class="token operator">></span> <span class="token number">0</span> <span class="token keyword">ORDER</span> <span class="token keyword">BY</span> attnum<span class="token punctuation">;</span>

           attname            <span class="token operator">|</span> attnum <span class="token operator">|</span> attstorage <span class="token operator">|</span> attnotnull <span class="token operator">|</span> attisdropped 
<span class="token comment">------------------------------+--------+------------+------------+--------------</span>
 i_1                          <span class="token operator">|</span>      <span class="token number">1</span> <span class="token operator">|</span> p          <span class="token operator">|</span> t          <span class="token operator">|</span> f
 <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>pg<span class="token punctuation">.</span>dropped<span class="token punctuation">.</span><span class="token number">2.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token operator">|</span>      <span class="token number">2</span> <span class="token operator">|</span> p          <span class="token operator">|</span> f          <span class="token operator">|</span> t
<span class="token punctuation">(</span><span class="token number">2</span> <span class="token keyword">rows</span><span class="token punctuation">)</span></code></pre>
<p>When dropping a column,</p>
<ul class="list">
<li>the name becomes ‘.’ + ‘pg.dropped.’ + attnum + ‘.’,</li>
<li>the column becomes NULLable,</li>
<li>the column is marked as dropped.</li>
</ul>
<h3 id="iterating-add-/-drop-column"><a class="heading-anchor" href="#iterating-add-/-drop-column">Iterating ADD / DROP COLUMN</a></h3>
<p>One can wonder if there is a limit to the number of add/drop operations that can be run on a given table.</p>
<p>As usual, let us try:</p>
<pre class="language-sql"><code class="language-sql"><span class="token comment">-- ADD / DROP COLUMN example</span>
<span class="token keyword">DO</span> $$             
<span class="token keyword">DECLARE</span>
    i <span class="token keyword">int</span><span class="token punctuation">;</span>
<span class="token keyword">BEGIN</span>
    <span class="token keyword">EXECUTE</span> <span class="token string">'DROP TABLE IF EXISTS tadc;'</span><span class="token punctuation">;</span>
    <span class="token keyword">EXECUTE</span> <span class="token string">'CREATE TABLE tadc(i_1 int);'</span><span class="token punctuation">;</span>
    <span class="token keyword">FOR</span> i <span class="token operator">IN</span> <span class="token number">2.</span><span class="token number">.1601</span> <span class="token keyword">LOOP</span>
        <span class="token keyword">EXECUTE</span> <span class="token function">format</span><span class="token punctuation">(</span><span class="token string">'ALTER TABLE tadc ADD COLUMN i_%s int;'</span><span class="token punctuation">,</span> i<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">EXECUTE</span> <span class="token function">format</span><span class="token punctuation">(</span><span class="token string">'ALTER TABLE tadc DROP COLUMN i_%s;'</span><span class="token punctuation">,</span> i<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token keyword">END</span> <span class="token keyword">LOOP</span><span class="token punctuation">;</span>
<span class="token keyword">END</span> $$<span class="token punctuation">;</span></code></pre>
<p>The output is:</p>
<pre class="language-plaintext"><code class="language-plaintext">ERROR:  tables can have at most 1600 columns
CONTEXT:  SQL statement "ALTER TABLE tadc ADD COLUMN i_1601 int;"
PL/pgSQL function inline_code_block line 8 at EXECUTE</code></pre>
<p>Oh oh! We reached the 1,600 limit here as well.</p>
<p>Let us explore a bit after add/drop column 1,599 times:</p>
<pre class="language-sql"><code class="language-sql"><span class="token operator">=</span><span class="token comment"># SELECT attname,attnum,attstorage,attnotnull,attisdropped </span>
   <span class="token keyword">FROM</span> pg_attribute 
   <span class="token keyword">WHERE</span> attrelid<span class="token operator">=</span><span class="token punctuation">(</span>
                   <span class="token keyword">SELECT</span> oid 
                   <span class="token keyword">FROM</span> pg_class 
                   <span class="token keyword">WHERE</span> relname<span class="token operator">=</span><span class="token string">'tadc'</span>
                   <span class="token punctuation">)</span> 
     <span class="token operator">AND</span> attnum <span class="token operator">></span> <span class="token number">0</span> <span class="token keyword">ORDER</span> <span class="token keyword">BY</span> attnum<span class="token punctuation">;</span>

             attname             <span class="token operator">|</span> attnum <span class="token operator">|</span> attstorage <span class="token operator">|</span> attnotnull <span class="token operator">|</span> attisdropped 
<span class="token comment">---------------------------------+--------+------------+------------+--------------</span>
 i_1                             <span class="token operator">|</span>      <span class="token number">1</span> <span class="token operator">|</span> p          <span class="token operator">|</span> t          <span class="token operator">|</span> f
 <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>pg<span class="token punctuation">.</span>dropped<span class="token punctuation">.</span><span class="token number">2.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>    <span class="token operator">|</span>      <span class="token number">2</span> <span class="token operator">|</span> p          <span class="token operator">|</span> f          <span class="token operator">|</span> t
 <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>pg<span class="token punctuation">.</span>dropped<span class="token punctuation">.</span><span class="token number">3.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>    <span class="token operator">|</span>      <span class="token number">3</span> <span class="token operator">|</span> p          <span class="token operator">|</span> f          <span class="token operator">|</span> t
 <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>pg<span class="token punctuation">.</span>dropped<span class="token punctuation">.</span><span class="token number">4.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>    <span class="token operator">|</span>      <span class="token number">4</span> <span class="token operator">|</span> p          <span class="token operator">|</span> f          <span class="token operator">|</span> t
 <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>pg<span class="token punctuation">.</span>dropped<span class="token punctuation">.</span><span class="token number">5.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>    <span class="token operator">|</span>      <span class="token number">5</span> <span class="token operator">|</span> p          <span class="token operator">|</span> f          <span class="token operator">|</span> t

 <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>pg<span class="token punctuation">.</span>dropped<span class="token punctuation">.</span><span class="token number">1599.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token operator">|</span>   <span class="token number">1599</span> <span class="token operator">|</span> p          <span class="token operator">|</span> f          <span class="token operator">|</span> t
 <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>pg<span class="token punctuation">.</span>dropped<span class="token punctuation">.</span><span class="token number">1600.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token operator">|</span>   <span class="token number">1600</span> <span class="token operator">|</span> p          <span class="token operator">|</span> f          <span class="token operator">|</span> t
<span class="token punctuation">(</span><span class="token number">1600</span> <span class="token keyword">rows</span><span class="token punctuation">)</span></code></pre>
<p>Well, table <code>tadc</code> has 1,600 columns. You can see this as modifications are appending and table content rewriting is avoided.</p>
<p>At this point, further column add &amp; drop modifications will fail.</p>
<p>Is there anything I can do to escape this situation?</p>
<h4 id="the-vacuum-knight-shall-save-the-postgresql-princess-right"><a class="heading-anchor" href="#the-vacuum-knight-shall-save-the-postgresql-princess-right">The VACUUM knight shall save the PostgreSQL princess, right?</a></h4>
<p>The <code>VACUUM</code> command operates at the tuple level so even if you run a <code>VACUUM FULL</code> the table structure will not change.</p>
<h4 id="so-the-dragon-ate-the-knight-whats-next"><a class="heading-anchor" href="#so-the-dragon-ate-the-knight-whats-next">So, the dragon ate the knight, what’s next?</a></h4>
<p>This is not an issue with dead tuples but rather an issue with the catalog.<br>
You’ll need to create a new table definition.</p>
<p>Here are some solutions, from simple to complex:</p>
<ol class="list">
<li>
<p>Build a new table (requires service downtime)</p>
<ul class="list">
<li><code>CREATE TABLE (LIKE INCLUDING ALL)</code></li>
<li><code>COPY</code> data from old to new table</li>
<li>Rename tables</li>
<li>Drop old table</li>
</ul>
</li>
<li>
<p>Leverage logical replication (minimize service downtime)</p>
<ul class="list">
<li><code>CREATE TABLE LIKE (INCLUDING ALL)</code></li>
<li><code>CREATE local PUBLICATION/SUBSCRIPTION</code></li>
<li>Once data is synchronized, stop/pause application service</li>
<li>Drop subscription</li>
<li>Rename tables</li>
<li>Restart/resume application</li>
<li>Drop old table</li>
</ul>
</li>
</ol>
<h4 id="what-about-foreign-keys"><a class="heading-anchor" href="#what-about-foreign-keys">What about Foreign Keys?</a></h4>
<p>The above solution <em>works</em> fine for simple cases. But real life tables often<br>
use integrity constraints. Let’s explore a bit using foreign keys.</p>
<pre class="language-sql"><code class="language-sql"><span class="token comment">-- Foreign key case</span>

<span class="token operator">=</span><span class="token comment"># CREATE TABLE colors (id int, name text );</span>
<span class="token operator">=</span><span class="token comment"># CREATE TABLE objects ( id int, color_id int, name text );</span>

<span class="token operator">=</span><span class="token comment"># ALTER TABLE colors ADD PRIMARY KEY (id);</span>
<span class="token operator">=</span><span class="token comment"># ALTER TABLE objects ADD CONSTRAINT fk_color</span>
                       <span class="token keyword">FOREIGN</span> <span class="token keyword">KEY</span> <span class="token punctuation">(</span>color_id<span class="token punctuation">)</span> <span class="token keyword">REFERENCES</span> colors <span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token operator">=</span><span class="token comment"># INSERT INTO colors </span>
   <span class="token keyword">VALUES</span> <span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token string">'red'</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">,</span> <span class="token string">'green'</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token number">3</span><span class="token punctuation">,</span> <span class="token string">'blue'</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token operator">=</span><span class="token comment"># INSERT INTO objects </span>
   <span class="token keyword">VALUES</span> <span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">,</span><span class="token number">1</span><span class="token punctuation">,</span> <span class="token string">'red object'</span><span class="token punctuation">)</span>
         <span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token number">2</span><span class="token punctuation">,</span><span class="token number">2</span><span class="token punctuation">,</span> <span class="token string">'green object'</span><span class="token punctuation">)</span>
         <span class="token punctuation">,</span><span class="token punctuation">(</span><span class="token number">3</span><span class="token punctuation">,</span><span class="token number">3</span><span class="token punctuation">,</span><span class="token string">'blue object'</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Let’s apply the recipe:</p>
<pre class="language-sql"><code class="language-sql"><span class="token comment">-- Duplicate table structure (valid columns only)  and copy data</span>
<span class="token operator">=</span><span class="token comment"># CREATE TABLE tmp_colors (LIKE colors INCLUDING ALL);</span>
<span class="token operator">=</span><span class="token comment"># INSERT INTO tmp_colors SELECT * FROM colors;</span>

<span class="token comment">-- Do the DROP/RENAME trick</span>
<span class="token operator">=</span><span class="token comment"># BEGIN;</span>
<span class="token operator">=</span><span class="token comment"># DROP TABLE colors;</span>
<span class="token operator">=</span><span class="token comment"># ALTER TABLE tmp_colors RENAME TO colors;</span>
<span class="token operator">=</span><span class="token comment"># COMMIT;</span></code></pre>
<p>The <code>DROP TABLE</code> command issued an error:</p>
<pre class="language-plaintext"><code class="language-plaintext">ERROR:  cannot drop table colors because other objects depend on it
DETAIL:  constraint fk_color on table objects depends on table colors
HINT:  Use DROP ... CASCADE to drop the dependent objects too.</code></pre>
<p>As we can see, the recipe has to be changed to include dependent tables as well.</p>
<p>Adding <code>CASCADE</code> will drop FK constraints on dependent tables.</p>
<p>Let’s run a modified version of the recipe:</p>
<pre class="language-sql"><code class="language-sql"><span class="token comment">-- Do the DROP/RENAME trick</span>
<span class="token operator">=</span><span class="token comment"># BEGIN;</span>

<span class="token operator">=</span><span class="token comment"># DROP TABLE colors CASCADE;  -- DROP related FOREIGN KEY constaints</span>

<span class="token operator">=</span><span class="token comment"># ALTER TABLE tmp_colors RENAME TO colors;</span>

<span class="token comment">-- Recreate FK contraint</span>
<span class="token operator">=</span><span class="token comment"># ALTER TABLE objects ADD CONSTRAINT fk_color</span>
                       <span class="token keyword">FOREIGN</span> <span class="token keyword">KEY</span> <span class="token punctuation">(</span>color_id<span class="token punctuation">)</span> <span class="token keyword">REFERENCES</span> colors <span class="token punctuation">(</span>id<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">COMMIT</span><span class="token punctuation">;</span></code></pre>
<p>We have to check the behaviour is the expected one:</p>
<pre class="language-sql"><code class="language-sql"><span class="token operator">=</span><span class="token comment"># INSERT INTO objects VALUES (5,5,'ro');</span>
ERROR:  <span class="token keyword">insert</span> <span class="token operator">or</span> <span class="token keyword">update</span> <span class="token keyword">on</span> <span class="token keyword">table</span> <span class="token string">"objects"</span> violates <span class="token keyword">foreign</span> <span class="token keyword">key</span> <span class="token keyword">constraint</span> <span class="token string">"fk_color"</span>
DETAIL:  <span class="token keyword">Key</span> <span class="token punctuation">(</span>color_id<span class="token punctuation">)</span><span class="token operator">=</span><span class="token punctuation">(</span><span class="token number">5</span><span class="token punctuation">)</span> <span class="token operator">is</span> <span class="token operator">not</span> present <span class="token operator">in</span> <span class="token keyword">table</span> <span class="token string">"colors"</span><span class="token punctuation">.</span>

<span class="token operator">=</span><span class="token comment"># INSERT INTO objects VALUES (5,3,'ro');</span>
<span class="token keyword">INSERT</span> <span class="token number">0</span> <span class="token number">1</span></code></pre>
<p>Success!</p>
<p>When integrity constraints are too numerous or you find it difficult to follow,<br>
you may use pg_dump/pg_restore to rebuild all automatically. If service downtime<br>
is an issue, you may use logical replication to perform like pg_dump/pg_restore.</p>
<h2 id="best-is-to-avoid-having-to-deal-with-this"><a class="heading-anchor" href="#best-is-to-avoid-having-to-deal-with-this">Best is to avoid having to deal with this</a></h2>
<p>As you can see, having to deal with the 1,600 column limit is not something you would<br>
like to do just for fun (usually). Notably, it can lead to service downtime.</p>
<h2 id="talk-to-us"><a class="heading-anchor" href="#talk-to-us">Talk to us</a></h2>
<p>Do you have other ideas of how to address this situation? Have you run into odd ways of reaching this hard-coded limit? <a href="https://www.data-bene.io/en/#contact" rel="noopener">Contact us</a>! We always love a good discussion about PostgreSQL.</p>
 ]]></content>
			<author>
				<name>Frédéric Delacourt</name>
			</author>
    </entry>
    <entry>
      <title>Cumulative Statistics in PostgreSQL 18</title>
      <link href="https://www.data-bene.io/en/blog/cumulative-statistics-in-postgresql-18/" />
      <updated>2025-09-29T00:00:00Z</updated>
      <id>https://www.data-bene.io/en/blog/cumulative-statistics-in-postgresql-18/</id>
     <content type="html"><![CDATA[ <p>In <strong>PostgreSQL 18</strong>, the statistics &amp; monitoring subsystem receives a significant overhaul - extended cumulative statistics, new per-backend I/O visibility, the ability for extensions to export / import / adjust statistics, and improvements to GUC controls and snapshot / caching behavior. These changes open new doors for performance analysis, cross‑environment simulation, and tighter integration with extensions. In this article I explore what’s new, what to watch out for, Grand Unified Configuration (GUC) knobs, and how extension authors can leverage the new C API surface.</p>
<h2 id="introduction-and-motivation"><a class="heading-anchor" href="#introduction-and-motivation">Introduction &amp; motivation</a></h2>
<p>Statistics (in the broad sense: monitoring counters, I/O metrics, and planner / optimizer estimates) lie at the heart of both performance tuning and internal decision making in PostgreSQL. Transparent, reliable, and manipulable statistics, among other things, allow DBAs to address the efficiency of PostgreSQL directly, as well as enable “extensions” to improve the user experience.</p>
<p>That said, the historic statistics system of PostgreSQL has not been without points of friction. These include limited ability to clear (relations) statistics, metrics with units that don’t always align with user goals, and no C API for using the PostgreSQL Cumulative Stats engine. PostgreSQL 18 addresses these concerns head on.</p>
<p>Below is a summary of the key enhancements.</p>
<h2 id="a-warning-on-stats"><a class="heading-anchor" href="#a-warning-on-stats">A warning on stats</a></h2>
<p>While statistics offer incredible value, their collection can take up significant time and resources. PostgreSQL 18 introduces an important consideration: with the expanded range of collectible metrics, the hash table maximum size has been increased. Do keep in mind, especially if you’re designing large-scale systems with table-per-customer architectures, that 1GB ceilings have been shown to be hit with some millions of tables.</p>
<h2 id="whats-new-with-postgresql-18-and-stats"><a class="heading-anchor" href="#whats-new-with-postgresql-18-and-stats">What’s new with PostgreSQL 18 and “stats”</a></h2>
<p>Here are the major new or improved features relating to statistics and monitoring. Each item links to the relevant documentation or code where possible.</p>
<p>Generally, <a href="https://www.postgresql.org/docs/18/monitoring-stats.html#MONITORING-PG-STAT-IO-VIEW" rel="noopener">pg_stat_io</a> now reports I/O activity in bytes rather than pages, which is more convenient for analysis. Moreover, WAL statistics were moved here from <code>pg_stat_wal</code>, providing a single, comprehensive view.</p>
<h3 id="upgrades"><a class="heading-anchor" href="#upgrades">Upgrades</a></h3>
<p><a href="https://www.postgresql.org/docs/18/pgupgrade.html" rel="noopener">pg_upgrade</a> is now able to retain optimizer statistics, removing the need to run a full <code>ANALYZE</code> on the databases to get good planning of queries after the upgrade; this is a very welcome update for large databases! Be aware that custom statistics added by an extension along with those created with <a href="https://www.postgresql.org/docs/18/sql-createstatistics.html" rel="noopener">CREATE STATISTICS</a> won’t be retained.</p>
<p>You will surely want to look at new options in <a href="https://www.postgresql.org/docs/18/app-vacuumdb.html" rel="noopener">vacuumdb</a> (<code>--missing-stats-only</code>) to, well, analyze only what’s needed.</p>
<p>On a similar note, the <code>--[no-]statistics</code> flag has been added to <a href="https://www.postgresql.org/docs/18/app-pgdump.html" rel="noopener">pg_dump</a>, <a href="https://www.postgresql.org/docs/18/app-pgdumpall.html" rel="noopener">pg_dumpall</a>, and <a href="https://www.postgresql.org/docs/18/app-pgrestore.html" rel="noopener">pg_restore</a>.</p>
<h3 id="maintenance"><a class="heading-anchor" href="#maintenance">Maintenance</a></h3>
<p>It’s now easier to know the maintenance effort on objects with total time spent on VACUUM and ANALYZE operation (and automatic ones) now reported into <a href="https://www.postgresql.org/docs/18/monitoring-stats.html#MONITORING-PG-STAT-ALL-TABLES-VIEW" rel="noopener">pg_stat_all_tables</a> and variants.</p>
<p>A new GUC to not forget is <a href="https://www.postgresql.org/docs/18/runtime-config-statistics.html#GUC-TRACK-COST-DELAY-TIMING" rel="noopener">track_cost_delay_timing</a>. It collects time spent sleeping (due to delayed operations) for <code>VACUUM</code> and <code>ANALYZE</code>. While very interesting, like other <code>track_io*</code> GUCs, it implies a lot of extra calls to the system clock which on some platforms can lead to a severe performance impact. Always check with tool like <a href="https://www.postgresql.org/docs/18/pgtesttiming.html" rel="noopener">pg_test_timing</a> to ensure your system can afford it!</p>
<p>No more questions about checkpointer activity when using <a href="https://www.postgresql.org/docs/18/monitoring-stats.html#MONITORING-PG-STAT-CHECKPOINTER-VIEW" rel="noopener">pg_stat_checkpointer</a>. The new attribute <code>num_done</code> lets us know the number of <strong>completed</strong> checkpoints. You can also get what kind of buffers were written with <code>slru_written</code> and <code>buffers_written</code> now only matching <code>shared_buffers</code>: previously log and view were not providing the same counts because there was a SLRU counter <a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=17cc5f666" rel="noopener">in one case and not the other</a>.</p>
<h3 id="analysis"><a class="heading-anchor" href="#analysis">Analysis</a></h3>
<p>Want to know more about the I/O handled by the backend (PID)? Call <a href="https://www.postgresql.org/docs/18/monitoring-stats.html#PG-STAT-GET-BACKEND-IO" rel="noopener">pg_stat_get_backend_io(int)</a> and you’ll get output similar to what the <code>pg_stat_io</code> view provides, for this process (excluding those already). As for the WAL stats for this PID: call <a href="https://www.postgresql.org/docs/18/monitoring-stats.html#PG-STAT-GET-BACKEND-WAL" rel="noopener">pg_stat_get_backend_wal(int)</a>.</p>
<p>New attributes <code>parallel_workers_to_launch</code> and <code>parallel_workers_launched</code> were introduced in <a href="https://www.postgresql.org/docs/18/monitoring-stats.html#MONITORING-PG-STAT-DATABASE-VIEW" rel="noopener">pg_stat_database</a>. The ratio lets us know if we have enough slots for parallel workers.</p>
<p>Interesting changes on <a href="https://www.postgresql.org/docs/18/pgstatstatements.html" rel="noopener">pg_stat_statements</a>: more queries will be grouped under the same identifier. For example, patterns <code>IN (1,2,3, ...)</code> as only first and last constant will be used. A more counter-intuitive change is related to the table name used in a query. Only the name is used, not the schema or relation OID. This last change allows us to track dropped or recreated tables for example, but it will group statistics from unrelated tables if they have just the same name. The way to keep separate statistics for tables with same name is to alias them in the queries (<code>FROM my.table mt, other.table ot</code>)…</p>
<p>Finally, additions to <a href="https://www.postgresql.org/docs/18/view-pg-backend-memory-contexts.html" rel="noopener">pg_backend_memory_contexts</a> with <code>path</code> (to get parent/child) and <code>type</code> to segregate <code>AllocSet</code>, <code>Generation</code>, <code>Slab</code> and <code>Bump</code> contexts… and what exactly are <code>Slab</code> and <code>Bump</code>? They are not documented; for these you’ll want to <a href="https://github.com/postgres/postgres/tree/master/src/backend/utils/mmgr" rel="noopener">read headers of C files here</a>. They exist to optimize memory allocation, reallocation, and reset, depending on expected memory usage. For example, <code>Slab</code> is defined as a «MemoryContext implementation designed for cases where large numbers of equally-sized objects can be allocated and freed efficiently with minimal memory wastage and fragmentation».</p>
<p>Ah, no, a last one, <code>wal_buffers_full</code> was added to <code>pg_stat_statements</code> to allow us to tune for <code>wal_buffers</code> with better insights.</p>
<h3 id="replication"><a class="heading-anchor" href="#replication">Replication</a></h3>
<p>There are now better insights for conflict management when using logical replication that leverage new attributes in <a href="https://www.postgresql.org/docs/18/monitoring-stats.html#MONITORING-PG-STAT-SUBSCRIPTION-STATS" rel="noopener">pg_stat_subscription_stats</a>. As reference, this excerpt from <a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=6c2b5edec" rel="noopener">the commit entry</a> lists the following attributes that were introduced:</p>
<ul class="list">
<li>
<p><code>confl_insert_exists</code>:<br>
Number of times a row insertion violated a NOT DEFERRABLE unique<br>
constraint.</p>
</li>
<li>
<p><code>confl_update_origin_differs</code>:<br>
Number of times an update was performed on a row that was<br>
previously modified by another origin.</p>
</li>
<li>
<p><code>confl_update_exists</code>:<br>
Number of times that the updated value of a row violates a<br>
NOT DEFERRABLE unique constraint.</p>
</li>
<li>
<p><code>confl_update_missing</code>:<br>
Number of times that the tuple to be updated is missing.</p>
</li>
<li>
<p><code>confl_delete_origin_differs</code>:<br>
Number of times a delete was performed on a row that was<br>
previously modified by another origin.</p>
</li>
<li>
<p><code>confl_delete_missing</code>:<br>
Number of times that the tuple to be deleted is missing.</p>
</li>
</ul>
<h3 id="advanced"><a class="heading-anchor" href="#advanced">Advanced</a></h3>
<p>There is now a <a href="https://www.postgresql.org/docs/18/functions-admin.html#FUNCTIONS-ADMIN-STATSMOD" rel="noopener">new set of functions</a> to manage relation and attributes stats (<code>relpages</code>, <code>avg_width</code>, and so on). This gives you the freedom to export, import, and adjust stats as you want, so you can replicate planner behavior outside of “production”, maintain patched stats, and so on.</p>
<h3 id="my-favorite-for-extension-authors-the-new-c-stats-api"><a class="heading-anchor" href="#my-favorite-for-extension-authors-the-new-c-stats-api">My favorite for extension authors: the new C stats API</a></h3>
<p>One of the most exciting parts is what PostgreSQL 18 <em>opens up</em> for extension authors.</p>
<p>This tiny line at bottom of section <a href="https://www.postgresql.org/docs/18/release-18.html#RELEASE-18-MODULES" rel="noopener">E.1.3.9 Modules</a> is what concerns these changes:</p>
<blockquote>
<p>Allow extensions to use the server’s cumulative statistics API (Michael Paquier)</p>
</blockquote>
<p>Previously statistics manipulation was an internal-only affair; now there is an official, structured API surface you can build on (or wrap).</p>
<p>The <a href="https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=7949d9594" rel="noopener">commit message</a> is well written, and covers most of the new functionality. A subset of the options is <a href="https://www.postgresql.org/docs/18/xfunc-c.html#XFUNC-ADDIN-CUSTOM-CUMULATIVE-STATISTICS" rel="noopener">detailed in the documentation</a>. However, you will need to go into source code to know more at this stage; in particular, it’s worth having a look at the <code>injection points</code> extension (provided in core) which uses the new API.</p>
<p>For a deeper dive into how an extension can leverage these new capabilities, soon you will be able to see <strong>PACS (PostgreSQL Advanced Cumulative Statistics)</strong> on Codeberg - my project that provides a wrapper library and helper utilities around the new PostgreSQL 18 statistics APIs.</p>
<p>In the meantime, the talk I gave at <a href="https://archive.fosdem.org/2025/schedule/event/fosdem-2025-4496-stats-roll-baby-stats-roll-/" rel="noopener">FOSDEM 2025</a> explores these topics in greater detail.</p>
 ]]></content>
			<author>
				<name>Cédric Villemain</name>
			</author>
    </entry>
    <entry>
      <title>Once Upon a Time in a Confined Database - PostgreSQL, QRCodes, and the Art of Backup Without a Network</title>
      <link href="https://www.data-bene.io/en/blog/backup-without-a-network/" />
      <updated>2025-04-01T00:00:00Z</updated>
      <id>https://www.data-bene.io/en/blog/backup-without-a-network/</id>
     <content type="html"><![CDATA[ <h2 id="📦-the-fort-knox-of-databases"><a class="heading-anchor" href="#📦-the-fort-knox-of-databases">📦 The Fort Knox of Databases</a></h2>
<p>Once upon a time, in a faraway server room encased in heavy glass and reinforced concrete, lived a PostgreSQL database so confined, so secluded, it could only dream of the cloud.</p>
<p>No network.</p>
<p>No USB.</p>
<p>No writable external storage device.</p>
<p>Just a keyboard, a monitor, and the hum of industrial-grade air filters.</p>
<p>This wasn’t your average air-gapped setup. This was a zero-exfiltration zone, with operational security so tight you’d think it was guarding state secrets—or worse, legacy banking software.</p>
<p>And yet, in this digital oubliette, one innocent challenge remained:<br>
<strong>How do you back up a PostgreSQL database without ever extracting a file?</strong></p>
<hr>
<h2 id="🎥-when-screens-are-your-network"><a class="heading-anchor" href="#🎥-when-screens-are-your-network">🎥 When Screens Are Your Network</a></h2>
<p>Our customer didn’t just want backups—they <em>needed</em> them. The fear wasn’t theft, it was <strong>total failure</strong>: a motherboard dying quietly in its glass sarcophagus, taking the data with it. And if it came to that, chiseling through reinforced architecture wasn’t a viable disaster recovery plan.</p>
<p>We brainstormed everything:</p>
<ul class="list">
<li>OCR of scrolling SQL dumps? Too lossy.</li>
<li>Filming the <code>psql</code> output? Way too verbose.</li>
<li>Printing out hex? Please, we’re not monsters.</li>
</ul>
<p>And then came the epiphany: <strong>QR codes</strong>.</p>
<p>What if we could <code>pg_dump</code> the database…</p>
<p>into QR codes…</p>
<p>on the screen…</p>
<p>captured by a high-speed camera…</p>
<p>then reassembled frame by frame outside the vault?</p>
<p>It was so absurd, it just might work.</p>
<hr>
<h2 id="🧠-hacking-pg_dump-now-with-more-pixels"><a class="heading-anchor" href="#🧠-hacking-pg_dump-now-with-more-pixels">🧠 Hacking <code>pg_dump</code>: Now with More Pixels</a></h2>
<p>PostgreSQL’s beloved <code>pg_dump</code> tool is modular. So we extended it with a custom archiver: <code>--format=qrcode</code>.</p>
<p>Here’s how it works:</p>
<ol class="list">
<li>
<p><strong>QR encoding</strong>: Each chunk of SQL output is encoded into a PNG QR code.</p>
</li>
<li>
<p><strong>Streaming</strong>: Instead of saving to disk, we push the stream of PNGs directly to <code>stdout</code>.</p>
</li>
<li>
<p><strong>Framing</strong>: Our UI lays out multiple QR codes on a single screen using high-DPI output. (We’re talking 2000+ pixels here—room for a whole grid of codes.)</p>
</li>
<li>
<p><strong>Playback</strong>: A dedicated machine with a 1280Hz high-speed camera films the screen, capturing the sequence as a video.</p>
</li>
</ol>
<p>No keyboard macros. No sneaky uploads. Just photons and frames.</p>
<hr>
<h2 id="🔍-reassembly-outside-the-glass"><a class="heading-anchor" href="#🔍-reassembly-outside-the-glass">🔍 Reassembly Outside the Glass</a></h2>
<p>Once the video is extracted from the glass box:</p>
<ul class="list">
<li>
<p>Our parser watches the footage, frame by frame.</p>
</li>
<li>
<p>QR codes are detected and decoded in parallel.</p>
</li>
<li>
<p>Each chunk is sequence-tagged for ordering.</p>
</li>
<li>
<p>The resulting text is reassembled into a proper <code>pg_dump.sql</code>.</p>
</li>
</ul>
<p>And just like that: the database lives again—<strong>fully exported with no digital transfer</strong>.<br>
Only light and lenses.</p>
<hr>
<h2 id="🧩-notes-on-performance-and-fidelity"><a class="heading-anchor" href="#🧩-notes-on-performance-and-fidelity">🧩 Notes on Performance &amp; Fidelity</a></h2>
<ul class="list">
<li>
<p><strong>QR Version</strong>: We used Version 40 QR codes (max capacity) with optimized binary mode for high density.</p>
</li>
<li>
<p><strong>Error Correction</strong>: Level Q for resilience under compression/artifacts.</p>
</li>
<li>
<p><strong>Screen Real Estate</strong>: 25×16 grid of codes per frame on a 1920×1080 pixel monitor—350 chunks per screen, around 1,016KB per frame.</p>
</li>
<li>
<p><strong>Playback Rate</strong>: We achieved ~60 screens/sec = 21,000 chunks/sec, nearly 60MB/sec!.</p>
</li>
<li>
<p><strong>Total Export Time</strong>: A full logical backup under 6GB was exported in less than 100 minutes!</p>
</li>
</ul>
<hr>
<h2 id="🛡️-why-this-matters"><a class="heading-anchor" href="#🛡️-why-this-matters">🛡️ Why This Matters</a></h2>
<p>This isn’t just a quirky story—it’s a reminder that <strong>PostgreSQL’s flexibility extends even into the absurd</strong>. Air-gapped systems aren’t rare in defense, finance, or critical infrastructure. And when normal tooling fails, PostgreSQL’s pluggable architecture gives you room to innovate, even in the tightest constraints.</p>
<p>We at <strong>Data Bene</strong> live for this kind of challenge. Whether it’s optimizing query plans or designing data exfiltration methods that look like spycraft, we’re here to help you make PostgreSQL dance—even when it’s stuck in a cage.</p>
<hr>
<p><em>Want to try it yourself? Drop us a line—we love weird backups.</em></p>
<p><em>And if you’re thinking of streaming <code>pg_restore</code> into a laser light show, call us. We’re intrigued.</em></p>
 ]]></content>
			<author>
				<name>Cédric Villemain</name>
			</author>
    </entry>
    <entry>
      <title>Postgres Café: Expand monitoring capabilities with StatsMgr</title>
      <link href="https://www.data-bene.io/en/blog/postgres-cafe-expand-monitoring-capabilities-with-statsmgr/" />
      <updated>2025-01-07T00:00:00Z</updated>
      <id>https://www.data-bene.io/en/blog/postgres-cafe-expand-monitoring-capabilities-with-statsmgr/</id>
     <content type="html"><![CDATA[ <p>2025 has begun, and with it we’re excited to release the second episode of <a href="https://www.youtube.com/watch?v=WwaJd2c9whM" rel="noopener">Postgres Café</a>, a blog and video series from our teams over at <a href="https://www.data-bene.io/en/" rel="noopener">Data Bene</a> and <a href="https://xata.io/" rel="noopener">Xata</a> made with the intention of exploring the world of open source and where it meets PostgreSQL’s extensibility. Throughout this series, we discuss different extensions and tools that enhance the developer experience when working with PostgreSQL. In our second episode, we explore a brand new PostgreSQL extension called <a href="https://codeberg.org/data-bene/statsmgr" rel="noopener">StatsMgr</a> that leverages background workers and shared memory to snapshot, manage, and query various statistics for WAL, SLRU, IO, checkpointing, and more.</p>
<h2 id="episode-2-statsmgr"><a class="heading-anchor" href="#episode-2-statsmgr">Episode 2: StatsMgr</a></h2>
<p>In this episode, we introduce the just-released open source extension StatsMgr, created to continuously monitor and track events across PostgreSQL and the underlying system. Here’s a look at what this episode covered:</p>
<h3 id="customized-metrics-processing"><a class="heading-anchor" href="#customized-metrics-processing">Customized metrics processing</a></h3>
<p>Originally the idea was to provide a simplified interface for metrics, while enhancing them with a wide variety of available types. This functionality was then expanded to address problems like:</p>
<ul class="list">
<li><strong>Making statistics available</strong> for collection from external systems, without interruption even when those external systems are down.</li>
<li><strong>Providing an immediate view of PostgreSQL statistics</strong> with historical tracking, including pg_stat views &amp; functions.</li>
<li><strong>Increasing &amp; reducing the amount of historical records when needed</strong> with dynamic buffer allocation.</li>
<li><strong>Debugging PostgreSQL instances</strong> with historical analysis and without required restarts.</li>
</ul>
<p>This extension, in turn, is great at handling situations like when…</p>
<ul class="list">
<li><strong>…your monitoring agent is down</strong>; using StatsMgr as a backup allows you to ensure you won’t lose statistics in this event, as events are captured regardless and stored for collection later on by your monitoring agent.</li>
<li><strong>…you have spikes or otherwise unusual behavior on your production system</strong>. This extension allows you to get an overview of activity for useful debugging insights.</li>
</ul>
<h3 id="expansive-and-historical-metrics-collection"><a class="heading-anchor" href="#expansive-and-historical-metrics-collection">Expansive &amp; historical metrics collection</a></h3>
<p>Currently, supported statistics include:</p>
<ul class="list">
<li>WAL</li>
<li>SLRU</li>
<li>BGWriter</li>
<li>Checkpointer</li>
<li>Archiver</li>
<li>IO</li>
</ul>
<p>Each of these is registered with a handler that lets you fetch and manage these statistics, and also is accompanied by shared memory structures for storing historical snapshots.</p>
<p>Some of the next steps for the project will include adding in dynamic statistics such as pg_stat_user_tables, amongst others.</p>
<p>There are still many things to do, from subtle improvements to major new features. So of course there’s many opportunities to contribute to the project, no matter if you’re a new-comer or an advanced PostgreSQL developer. Interested in being a part of the effort? Check out <a href="https://codeberg.org/Data-Bene/StatsMgr/src/branch/main/CONTRIBUTING.md" rel="noopener">CONTRIBUTING.md</a> within the project.</p>
<h3 id="watch-the-full-episode"><a class="heading-anchor" href="#watch-the-full-episode">Watch the full episode</a></h3>
<p>For an in-depth exploration of StatsMgr and its capabilities, watch the full episode here:</p>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/UMzCLFwCPI8?si=-NW4Na4PAiq6qdoY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
<h3 id="stay-tuned-for-more-postgres-tools"><a class="heading-anchor" href="#stay-tuned-for-more-postgres-tools">Stay tuned for more Postgres tools</a></h3>
<p>We still have much more to come for Postgres Café. <a href="https://www.youtube.com/playlist?list=PLf7KS0svgDP_zJmby3RMzzOVO45qLbruA" rel="noopener">Subscribe to the playlist</a> for episodes that feature more open-source tools like <a href="https://pgroll.com/" rel="noopener">pgroll</a> for zero-downtime schema migrations, <a href="https://www.citusdata.com/" rel="noopener">Citus Data</a> for distributed and scalable PostgreSQL as an extension, and more. Watch this space to learn how each tool can make working with Postgres smoother and more efficient.</p>
 ]]></content>
			<author>
				<name>Sarah Conway</name>
			</author>
    </entry>
    <entry>
      <title>Strange data type transformations</title>
      <link href="https://www.data-bene.io/en/blog/strange-data-type-transformations/" />
      <updated>2024-12-02T00:00:00Z</updated>
      <id>https://www.data-bene.io/en/blog/strange-data-type-transformations/</id>
     <content type="html"><![CDATA[ <h2 id="when-your-function-argument-types-are-loosely-changed"><a class="heading-anchor" href="#when-your-function-argument-types-are-loosely-changed">When your function argument types are loosely changed</a></h2>
<p>This article results from a code review I did for a customer.</p>
<p>Our customer created a <code>pg_dump --schema-only</code> of the target database to provide<br>
me with the plpgsql code and database object structures to review. So far<br>
so good.</p>
<p>I started to read the code and then became puzzled. The code looks like this:</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">CREATE</span> <span class="token keyword">FUNCTION</span> xxx<span class="token punctuation">(</span> p_id <span class="token keyword">character</span><span class="token punctuation">,</span> p_info <span class="token keyword">character</span> <span class="token keyword">varying</span> <span class="token punctuation">)</span>
<span class="token keyword">RETURNS</span> <span class="token keyword">integer</span>
<span class="token keyword">LANGUAGE</span> plpgsql
<span class="token keyword">AS</span> $$
<span class="token keyword">DECLARE</span>
<span class="token keyword">BEGIN</span>
   <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
   <span class="token keyword">INSERT</span> <span class="token keyword">INTO</span> t1
   <span class="token keyword">SELECT</span> <span class="token operator">*</span> <span class="token keyword">FROM</span> t2 <span class="token keyword">WHERE</span> t2<span class="token punctuation">.</span>id <span class="token operator">=</span> p_id<span class="token punctuation">;</span>
   <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
<span class="token keyword">END</span><span class="token punctuation">;</span>
$$
<span class="token punctuation">;</span></code></pre>
<p>Maybe you saw nothing wrong with the function. Perhaps knowing the table<br>
definition will help:</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">CREATE</span> <span class="token keyword">TABLE</span> t2 <span class="token punctuation">(</span>
   id <span class="token keyword">VARCHAR</span><span class="token punctuation">(</span><span class="token number">130</span><span class="token punctuation">)</span> <span class="token operator">NOT</span> <span class="token boolean">NULL</span>
   <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
   <span class="token keyword">PRIMARY</span> <span class="token keyword">KEY</span> <span class="token punctuation">(</span>id<span class="token punctuation">)</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p><a href="http://t2.id" rel="noopener">t2.id</a> is always 130 characters long (in practice) and there are 400 million tuples.<br>
So as you may have guessed, it seems odd to have the p_id CHARACTER matching id VARCHAR(130).<br>
Moreover CHARACTER is the same as CHAR(1).</p>
<p>Our customer had not seen any issues with the code for years. Nevertheless, our customer told me that the function definition he wrote was not like that: it was meant to be p_id CHARACTER(130) - not CHARACTER.</p>
<p>So what went wrong? Let’s test around because it’s fun.</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">CREATE</span> <span class="token keyword">FUNCTION</span> test<span class="token punctuation">(</span> c <span class="token keyword">character</span><span class="token punctuation">,</span> d <span class="token keyword">character</span> <span class="token keyword">varying</span> <span class="token punctuation">)</span>
<span class="token keyword">RETURNS</span> void
<span class="token keyword">LANGUAGE</span> plpgsql
<span class="token keyword">AS</span> $$
<span class="token keyword">BEGIN</span>
  RAISE NOTICE <span class="token string">'c=%, d=%'</span><span class="token punctuation">,</span> c<span class="token punctuation">,</span>d<span class="token punctuation">;</span>
<span class="token keyword">END</span><span class="token punctuation">;</span>
$$<span class="token punctuation">;</span>

<span class="token keyword">SELECT</span> test<span class="token punctuation">(</span> <span class="token string">'123465789'</span><span class="token punctuation">,</span> <span class="token string">'987654321'</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
NOTICE:  c<span class="token operator">=</span><span class="token number">123465789</span><span class="token punctuation">,</span> d<span class="token operator">=</span><span class="token number">987654321</span>
 test 
<span class="token comment">------</span>
 
<span class="token punctuation">(</span><span class="token number">1</span> <span class="token keyword">row</span><span class="token punctuation">)</span></code></pre>
<p>We have an interesting result here: no casting to CHAR(1) has been done.<br>
Let’s see more details:</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">EXPLAIN</span> <span class="token punctuation">(</span>COSTS <span class="token keyword">OFF</span><span class="token punctuation">,</span><span class="token keyword">ANALYZE</span><span class="token punctuation">,</span>VERBOSE<span class="token punctuation">)</span>
        <span class="token keyword">SELECT</span> test<span class="token punctuation">(</span> <span class="token string">'123465789'</span><span class="token punctuation">,</span> <span class="token string">'987654321'</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
NOTICE:  c<span class="token operator">=</span><span class="token number">123465789</span><span class="token punctuation">,</span> d<span class="token operator">=</span><span class="token number">987654321</span>
                             QUERY <span class="token keyword">PLAN</span>                              
<span class="token comment">---------------------------------------------------------------------</span>
 Result <span class="token punctuation">(</span>actual <span class="token keyword">time</span><span class="token operator">=</span><span class="token number">0.040</span><span class="token punctuation">.</span><span class="token number">.0</span><span class="token number">.041</span> <span class="token keyword">rows</span><span class="token operator">=</span><span class="token number">1</span> loops<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">)</span>
   Output: test<span class="token punctuation">(</span><span class="token string">'123465789'</span>::bpchar<span class="token punctuation">,</span> <span class="token string">'987654321'</span>::<span class="token keyword">character</span> <span class="token keyword">varying</span><span class="token punctuation">)</span>
 Planning <span class="token keyword">Time</span>: <span class="token number">0.023</span> ms
 Execution <span class="token keyword">Time</span>: <span class="token number">0.053</span> ms
<span class="token punctuation">(</span><span class="token number">4</span> <span class="token keyword">rows</span><span class="token punctuation">)</span></code></pre>
<p>We can see there was a cast to BPCHAR. As a reminder, BPCHAR is an alias of CHARACTER<br>
and it can represent a string up to 10,485,760 characters.</p>
<p>Now let’s make another test:</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">CREATE</span> <span class="token keyword">FUNCTION</span> test<span class="token punctuation">(</span>c <span class="token keyword">character</span><span class="token punctuation">(</span><span class="token number">4</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token keyword">RETURNS</span> <span class="token keyword">character</span><span class="token punctuation">(</span><span class="token number">4</span><span class="token punctuation">)</span>
<span class="token keyword">LANGUAGE</span> <span class="token keyword">sql</span>
<span class="token keyword">AS</span> $$
<span class="token keyword">select</span> c<span class="token punctuation">;</span>
$$<span class="token punctuation">;</span></code></pre>
<p>As you can see, the language changed to SQL and the argument type and the return<br>
type are CHAR(4). How does it execute?</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">SELECT</span> test<span class="token punctuation">(</span><span class="token string">'123456789'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
   test    
<span class="token comment">-----------</span>
 <span class="token number">123456789</span>
<span class="token punctuation">(</span><span class="token number">1</span> <span class="token keyword">row</span><span class="token punctuation">)</span>

<span class="token keyword">EXPLAIN</span> VERBOSE <span class="token keyword">SELECT</span> test<span class="token punctuation">(</span><span class="token string">'123456789'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
                QUERY <span class="token keyword">PLAN</span>                 
<span class="token comment">-------------------------------------------</span>
 Result  <span class="token punctuation">(</span>cost<span class="token operator">=</span><span class="token number">0.00</span><span class="token punctuation">.</span><span class="token number">.0</span><span class="token number">.01</span> <span class="token keyword">rows</span><span class="token operator">=</span><span class="token number">1</span> width<span class="token operator">=</span><span class="token number">32</span><span class="token punctuation">)</span>
   Output: <span class="token string">'123456789'</span>::bpchar
<span class="token punctuation">(</span><span class="token number">2</span> <span class="token keyword">rows</span><span class="token punctuation">)</span></code></pre>
<p>As you can see, even though you expect to process CHAR(4) data, you end up processing arbitrary length strings instead!!</p>
<p>However, do not rush to PostgreSQL mailing list to complain YET!</p>
<p>As a matter of fact, this behaviour is not a bug. The <a href="https://www.postgresql.org/docs/current/sql-createfunction.html" rel="noopener">documentation</a> states:</p>
<blockquote>
<p>“The full SQL type syntax is allowed for declaring a function’s arguments and return value. However, parenthesized type modifiers (e.g., the precision field for type numeric) are discarded by CREATE FUNCTION. Thus for example CREATE FUNCTION foo (varchar(10)) … is exactly the same as CREATE FUNCTION foo (varchar) …”</p>
</blockquote>
<p>This explains that CHARACTER(x) became CHARACTER aliased as BPCHAR. And as we saw, BPCHAR is not actually CHAR(1) but more like VARCHAR(10485760). This fully explains the behaviour.</p>
<p>Wait, wait , WAIT ! The original intention was to deal with CHAR(4) string - not any arbituary length strings.</p>
<p>Isn’t there any hope? No, sorry… (kidding.)</p>
<p>Reading the same documentation page, we see that “argtype” and “rettype” can be base, composite, or domain types, or can reference the type of a table column.</p>
<p>The trick is to create either a composite type or a domain to use as argtype or rettype.</p>
<p>Here are some examples:</p>
<pre class="language-sql"><code class="language-sql"><span class="token comment">-- Works in simple case trick</span>
<span class="token keyword">SELECT</span> test<span class="token punctuation">(</span> <span class="token string">'12345789'</span>::<span class="token keyword">char</span><span class="token punctuation">(</span><span class="token number">4</span><span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">-- Domain trick</span>
<span class="token keyword">CREATE</span> DOMAIN c4 <span class="token keyword">AS</span> <span class="token keyword">char</span><span class="token punctuation">(</span><span class="token number">4</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">CREATE</span> <span class="token keyword">FUNCTION</span> test<span class="token punctuation">(</span>param c4<span class="token punctuation">)</span>
<span class="token keyword">RETURNS</span> c4
<span class="token keyword">AS</span> $$
<span class="token keyword">BEGIN</span>
  RAISE NOTICE <span class="token string">'param=%'</span><span class="token punctuation">,</span> param<span class="token punctuation">;</span>
  <span class="token keyword">RETURN</span> param<span class="token punctuation">;</span>
<span class="token keyword">END</span><span class="token punctuation">;</span>
$$ <span class="token keyword">LANGUAGE</span> plpgsql<span class="token punctuation">;</span>

<span class="token keyword">SELECT</span> test<span class="token punctuation">(</span> <span class="token string">'123456789'</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
ERROR:  <span class="token keyword">value</span> too long <span class="token keyword">for</span> <span class="token keyword">type</span> <span class="token keyword">character</span><span class="token punctuation">(</span><span class="token number">4</span><span class="token punctuation">)</span>

<span class="token keyword">SELECT</span> test<span class="token punctuation">(</span> <span class="token string">'123456789'</span>::<span class="token keyword">char</span><span class="token punctuation">(</span><span class="token number">4</span><span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
NOTICE:  param<span class="token operator">=</span><span class="token number">1234</span>
 test 
<span class="token comment">------</span>
 <span class="token number">1234</span>
<span class="token punctuation">(</span><span class="token number">1</span> <span class="token keyword">row</span><span class="token punctuation">)</span>

<span class="token keyword">SELECT</span> test<span class="token punctuation">(</span> <span class="token string">'123456789'</span>::c4<span class="token punctuation">)</span><span class="token punctuation">;</span>
NOTICE:  param<span class="token operator">=</span><span class="token number">1234</span>
 test 
<span class="token comment">------</span>
 <span class="token number">1234</span>
<span class="token punctuation">(</span><span class="token number">1</span> <span class="token keyword">row</span><span class="token punctuation">)</span>

<span class="token keyword">SELECT</span> pg_typeof<span class="token punctuation">(</span> test<span class="token punctuation">(</span> <span class="token string">'123456789'</span>::<span class="token keyword">char</span><span class="token punctuation">(</span><span class="token number">4</span><span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
NOTICE:  param<span class="token operator">=</span><span class="token number">1234</span>
 pg_typeof 
<span class="token comment">-----------</span>
 c4
<span class="token punctuation">(</span><span class="token number">1</span> <span class="token keyword">row</span><span class="token punctuation">)</span></code></pre>
<p>Now you should be happy with the result.</p>
<p>What? Not yet? Ok here is an additional trick.</p>
<pre class="language-sql"><code class="language-sql"><span class="token comment">-- Map a table structure</span>
<span class="token keyword">CREATE</span> <span class="token keyword">TABLE</span> qq <span class="token punctuation">(</span> c <span class="token keyword">char</span><span class="token punctuation">(</span><span class="token number">4</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">CREATE</span> <span class="token keyword">FUNCTION</span> test<span class="token punctuation">(</span><span class="token operator">IN</span> c qq<span class="token punctuation">,</span> <span class="token keyword">OUT</span> d qq<span class="token punctuation">)</span>
<span class="token keyword">LANGUAGE</span> <span class="token keyword">sql</span>
<span class="token keyword">AS</span> $$
<span class="token keyword">SELECT</span> c<span class="token punctuation">;</span>
$$<span class="token punctuation">;</span>

<span class="token keyword">SELECT</span> <span class="token operator">*</span> <span class="token keyword">FROM</span> test<span class="token punctuation">(</span><span class="token keyword">ROW</span><span class="token punctuation">(</span><span class="token string">'12345'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
ERROR:  <span class="token keyword">value</span> too long <span class="token keyword">for</span> <span class="token keyword">type</span> <span class="token keyword">character</span><span class="token punctuation">(</span><span class="token number">4</span><span class="token punctuation">)</span>

<span class="token keyword">SELECT</span> <span class="token operator">*</span> <span class="token keyword">from</span> test<span class="token punctuation">(</span><span class="token keyword">ROW</span><span class="token punctuation">(</span><span class="token string">'1234'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
  c   
<span class="token comment">------</span>
 <span class="token number">1234</span></code></pre>
<p>Hmm, OK, but how is this is different from the domain trick?</p>
<pre class="language-sql"><code class="language-sql"><span class="token comment">-- Easy Type Alteration</span>
<span class="token keyword">ALTER</span> <span class="token keyword">TABLE</span> qq <span class="token keyword">ALTER</span> c <span class="token keyword">TYPE</span> <span class="token keyword">char</span><span class="token punctuation">(</span><span class="token number">5</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">SELECT</span> <span class="token operator">*</span> <span class="token keyword">FROM</span> test<span class="token punctuation">(</span> <span class="token keyword">ROW</span><span class="token punctuation">(</span><span class="token string">'12345'</span><span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
   c   
<span class="token comment">-------</span>
 <span class="token number">12345</span></code></pre>
<p>Try to ALTER a domain - you will see how (not) easy it is.</p>
<p>The table definition trick allows for some flexibility as follows:</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">ALTER</span> <span class="token keyword">TABLE</span> qq <span class="token keyword">ADD</span> ee <span class="token keyword">int</span><span class="token punctuation">;</span>

<span class="token keyword">SELECT</span> test<span class="token punctuation">(</span> <span class="token keyword">ROW</span><span class="token punctuation">(</span><span class="token string">'12345'</span><span class="token punctuation">,</span> <span class="token number">4</span><span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
   test   
<span class="token comment">----------</span>
 <span class="token punctuation">(</span><span class="token number">12345</span><span class="token punctuation">,</span><span class="token number">4</span><span class="token punctuation">)</span>

<span class="token keyword">SELECT</span> <span class="token operator">*</span> <span class="token keyword">FROM</span> test<span class="token punctuation">(</span> <span class="token keyword">ROW</span><span class="token punctuation">(</span><span class="token string">'12345'</span><span class="token punctuation">,</span> <span class="token number">4</span><span class="token punctuation">)</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
   c   <span class="token operator">|</span> ee 
<span class="token comment">-------+----</span>
 <span class="token number">12345</span> <span class="token operator">|</span>  <span class="token number">4</span></code></pre>
<p>We hope you enjoyed this article and that you learnt something new and interesting!</p>
 ]]></content>
			<author>
				<name>Frédéric Delacourt</name>
			</author>
    </entry>
</feed>
