<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Postgresql on inetshell</title><link>https://inet.sh/en/tags/postgresql/</link><description>Recent content in Postgresql on inetshell</description><generator>Hugo</generator><language>en</language><lastBuildDate>Sat, 26 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://inet.sh/en/tags/postgresql/index.xml" rel="self" type="application/rss+xml"/><item><title>Importing dumps into Cloud SQL without errors: DEFINER and SUPER (ERROR 1227) on MySQL, extensions and owners on PostgreSQL</title><link>https://inet.sh/en/posts/gcp/cloud-sql-import-dumps-definer-gtid-extensions/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/gcp/cloud-sql-import-dumps-definer-gtid-extensions/</guid><description>A mysqldump or pg_dump file that works on any server fails when imported into Cloud SQL: ERROR 1227 &amp;#39;you need the SUPER privilege&amp;#39; from DEFINER clauses and SET @@GLOBAL.GTID_PURGED, &amp;#39;must be owner of extension plpgsql&amp;#39; and &amp;#39;role does not exist&amp;#39; on PostgreSQL. The export flags Cloud SQL expects, what to do with DEFINER clauses (create the users, strip them, or switch to INVOKER), the sed that comments out EXTENSION statements, the bucket permission, and the import with gcloud sql import sql.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<p><strong>MySQL</strong>, on the source server:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">mysqldump --databases app -h db.example -u dumper -p <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --hex-blob --single-transaction --set-gtid-purged<span class="o">=</span>OFF <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --default-character-set<span class="o">=</span>utf8mb4 --routines --triggers --events <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="p">|</span> sed -E <span class="s1">&#39;s/DEFINER=`[^`]+`@`[^`]+`//g&#39;</span> <span class="p">|</span> gzip &gt; app.sql.gz
</span></span></code></pre></td></tr></table>
</div>
</div><p><strong>PostgreSQL</strong>, on the source server:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">pg_dump app -h db.example -U dumper --format<span class="o">=</span>plain --no-owner --no-acl <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="p">|</span> sed -E <span class="s1">&#39;s/(DROP|CREATE|COMMENT ON) EXTENSION/-- \1 EXTENSION/g&#39;</span> <span class="p">|</span> gzip &gt; app.sql.gz
</span></span></code></pre></td></tr></table>
</div>
</div><p>And in both cases:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">SA</span><span class="o">=</span><span class="k">$(</span>gcloud sql instances describe my-instance --format<span class="o">=</span><span class="s1">&#39;value(serviceAccountEmailAddress)&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">gcloud storage buckets add-iam-policy-binding gs://my-dumps <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  --member<span class="o">=</span><span class="s2">&#34;serviceAccount:</span><span class="nv">$SA</span><span class="s2">&#34;</span> --role<span class="o">=</span>roles/storage.objectAdmin
</span></span><span class="line"><span class="cl">gcloud storage cp app.sql.gz gs://my-dumps/
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">gcloud sql import sql my-instance gs://my-dumps/app.sql.gz --database<span class="o">=</span>app                 <span class="c1"># MySQL</span>
</span></span><span class="line"><span class="cl">gcloud sql import sql my-instance gs://my-dumps/app.sql.gz --database<span class="o">=</span>app --user<span class="o">=</span>postgres  <span class="c1"># PostgreSQL</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>gcloud sql import sql</code> takes plain SQL or <code>.gz</code>: the <em>custom</em> format of <code>pg_dump</code> goes through <code>pg_restore</code> (below).</p>
<h2 id="the-errors-and-where-they-come-from">The errors and where they come from</h2>
<p>On Cloud SQL nobody has <code>SUPER</code> (MySQL) or is a superuser (PostgreSQL); the admin is <code>root@%</code> or <code>postgres</code>, a member of <code>cloudsqlsuperuser</code>. Everything a normal dump takes for granted on your own server breaks right there:</p>
<table>
  <thead>
      <tr>
          <th>Import error</th>
          <th>Engine</th>
          <th>Cause</th>
          <th>Fix</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ERROR 1227 (42000) at line N: Access denied; you need (at least one of) the SUPER privilege(s) for this operation</code> (on a <code>CREATE … VIEW</code>, <code>TRIGGER</code>, <code>PROCEDURE</code>, <code>FUNCTION</code> or <code>EVENT</code> line)</td>
          <td>MySQL</td>
          <td><code>DEFINER=`user`@`host` </code> for a user that does not exist on the instance, or for <code>root@localhost</code>, which never exists on Cloud SQL. The dump does not carry users (and must not)</td>
          <td>Create those users before importing, or strip the <code>DEFINER=</code> clauses with the <code>sed</code></td>
      </tr>
      <tr>
          <td>The same <code>ERROR 1227</code> in the first lines, at <code>SET @@GLOBAL.GTID_PURGED=…</code></td>
          <td>MySQL</td>
          <td>The source has GTID enabled and <code>mysqldump</code> writes the position, which only <code>SUPER</code> can set</td>
          <td><code>--set-gtid-purged=OFF</code></td>
      </tr>
      <tr>
          <td><code>ERROR 1449 (HY000): The user specified as a definer ('app'@'localhost') does not exist</code> when <strong>using</strong> the view, not when importing</td>
          <td>MySQL</td>
          <td>You left a <code>DEFINER</code> for a user that does not exist on the instance</td>
          <td>Strip the <code>DEFINER</code> or create the user with that exact host</td>
      </tr>
      <tr>
          <td><code>ERROR: must be owner of extension plpgsql</code></td>
          <td>PostgreSQL</td>
          <td>The dump&rsquo;s <code>COMMENT ON EXTENSION plpgsql</code>: on Cloud SQL extensions belong to <code>cloudsqladmin</code></td>
          <td>Comment out the <code>EXTENSION</code> lines with the <code>sed</code></td>
      </tr>
      <tr>
          <td><code>ERROR: role &quot;app_owner&quot; does not exist</code></td>
          <td>PostgreSQL</td>
          <td><code>ALTER … OWNER TO</code> and <code>GRANT … TO</code> for roles from the old server</td>
          <td><code>--no-owner --no-acl</code>, or create the roles first</td>
      </tr>
      <tr>
          <td><code>ERROR: permission denied for schema public</code> or <code>must be owner of …</code> halfway through</td>
          <td>PostgreSQL</td>
          <td>You imported with a <code>--user</code> that does not own what already exists in the database</td>
          <td>Import as <code>postgres</code> into an empty database; reassign afterwards</td>
      </tr>
  </tbody>
</table>
<h2 id="mysql-what-to-do-with-definer-clauses">MySQL: what to do with DEFINER clauses</h2>
<p>First see how many there are on the source, and whose:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">DISTINCT</span><span class="w"> </span><span class="k">DEFINER</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">INFORMATION_SCHEMA</span><span class="p">.</span><span class="n">VIEWS</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">DISTINCT</span><span class="w"> </span><span class="k">DEFINER</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">INFORMATION_SCHEMA</span><span class="p">.</span><span class="n">TRIGGERS</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">DISTINCT</span><span class="w"> </span><span class="k">DEFINER</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">INFORMATION_SCHEMA</span><span class="p">.</span><span class="n">ROUTINES</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">DISTINCT</span><span class="w"> </span><span class="k">DEFINER</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">INFORMATION_SCHEMA</span><span class="p">.</span><span class="n">EVENTS</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Three ways out, from most to least faithful to the original:</p>
<ol>
<li><strong>Create the users on the instance before importing</strong>, with the same <code>user@host</code>: <code>gcloud sql users create app --host=% --instance=my-instance --password=…</code>. This is what the documentation recommends, and it keeps views and procedures exactly as they were. <code>root@localhost</code> is the exception: it cannot be created on Cloud SQL, so a <code>DEFINER</code> with that value must be changed no matter what.</li>
<li><strong>Strip the <code>DEFINER</code></strong> with the <code>sed</code> above: objects get created with the importing user as definer. Enough when the source users will never exist on Cloud SQL.</li>
<li><strong>Switch to <code>SQL SECURITY INVOKER</code></strong> (<code>sed -E 's/SQL SECURITY DEFINER/SQL SECURITY INVOKER/g'</code> on top of the previous one): the view or procedure runs with the caller&rsquo;s privileges and the definer stops mattering. It changes the permission model; check that the application users can read the underlying tables.</li>
</ol>
<p>The <code>sed</code> is deliberately broad because <code>DEFINER=</code> also shows up inside conditional comments (<code>/*!50013 DEFINER=… SQL SECURITY DEFINER */</code>); deleting just the <code>DEFINER=</code> leaves the rest intact and valid.</p>
<h2 id="mysql-the-flags-one-by-one">MySQL: the flags, one by one</h2>
<table>
  <thead>
      <tr>
          <th>Flag</th>
          <th>Why</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>--hex-blob</code></td>
          <td>Binary columns as hex; without it, a <code>BLOB</code> with odd bytes gets corrupted going through the character set</td>
      </tr>
      <tr>
          <td><code>--single-transaction</code></td>
          <td>Consistent InnoDB snapshot without locking tables; a must in production</td>
      </tr>
      <tr>
          <td><code>--set-gtid-purged=OFF</code></td>
          <td>Do not write <code>SET @@GLOBAL.GTID_PURGED</code>, which Cloud SQL will not let you run</td>
      </tr>
      <tr>
          <td><code>--default-character-set=utf8mb4</code></td>
          <td>Make the dump and the <code>SET NAMES</code> it carries match what is in the tables; avoids the classic <code>Ã©</code></td>
      </tr>
      <tr>
          <td><code>--databases app</code></td>
          <td>Required per the documentation, and never with the <code>mysql</code> database. Adds <code>CREATE DATABASE IF NOT EXISTS</code> and <code>USE app</code>, so <code>--database</code> on import becomes optional</td>
      </tr>
      <tr>
          <td><code>--routines --triggers --events</code></td>
          <td><code>mysqldump</code> only includes triggers by default; procedures, functions and events must be asked for. All of them carry a <code>DEFINER</code></td>
      </tr>
  </tbody>
</table>
<p>If the user you dump with lacks <code>PROCESS</code>, <code>mysqldump</code> 8.0.21+ fails right away with <em>&ldquo;Access denied; you need (at least one of) the PROCESS privilege(s)&rdquo;</em> while querying tablespaces: add <code>--no-tablespaces</code>.</p>
<h2 id="postgresql-plain-or-custom">PostgreSQL: plain or custom</h2>
<p><code>--no-owner --no-acl</code> make everything imported belong to <code>--user</code>, with no <code>GRANT</code> to phantom roles. If the application connects as another role, create the role first and use <code>--user=app</code>, or reassign afterwards:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="n">REASSIGN</span><span class="w"> </span><span class="n">OWNED</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">postgres</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">app</span><span class="p">;</span><span class="w">   </span><span class="c1">-- connected as postgres, in the imported database
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Create the extensions the database needs (<code>pg_trgm</code>, <code>postgis</code>, <code>uuid-ossp</code>…) <strong>before</strong> importing, as <code>postgres</code>, in the target database: <code>CREATE EXTENSION IF NOT EXISTS pg_trgm;</code>. Only the ones on Cloud SQL&rsquo;s supported extensions list work; <code>plpgsql</code> is already there. With that in place, commenting out the dump&rsquo;s <code>CREATE EXTENSION</code> lines removes nothing.</p>
<p>If you prefer the <em>custom</em> format (<code>-Fc</code>), the extension filter goes into the <code>pg_restore</code> table of contents, exactly as Google documents it, and you restore through the Cloud SQL Auth Proxy:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">pg_dump app -h db.example -U dumper --format<span class="o">=</span>custom --no-owner --no-acl &gt; app.dmp
</span></span><span class="line"><span class="cl">pg_restore --list app.dmp <span class="p">|</span> sed -E <span class="s1">&#39;s/(.*EXTENSION )/; \1/g&#39;</span> &gt; app.toc
</span></span><span class="line"><span class="cl">pg_restore --use-list<span class="o">=</span>app.toc --no-owner --no-acl -j <span class="m">4</span> -h 127.0.0.1 -U postgres -d app app.dmp
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>-j 4</code> restores in parallel, which the import from the bucket does not do.</p>
<h2 id="traps">Traps</h2>
<ul>
<li><strong>The bucket role goes to the instance&rsquo;s service account</strong>, not yours: the docs ask for <code>storage.objectAdmin</code> (for import alone <code>storage.objects.get</code> and <code>list</code>, that is <code>objectViewer</code>, are enough). Without it, <em>&ldquo;The service account does not have the required permissions for the bucket&rdquo;</em> even though you can read the object yourself.</li>
<li><strong>One operation at a time per instance</strong>: with a backup or export running, the import fails with <em>&ldquo;operation already in progress&rdquo;</em>. <code>gcloud sql operations list --instance=my-instance</code> shows it.</li>
<li><strong>Large dumps</strong>: the import is serial and slow; launch it with <code>--async</code> and watch it with <code>gcloud sql operations wait OPERATION_ID --timeout=unlimited</code>, or <code>gcloud</code> dies on you along the way.</li>
<li><strong><code>pg_dump</code> version</strong>: use the same major version as the target or lower. <code>pg_dump</code> 17 writes <code>SET transaction_timeout = 0;</code>, which a PostgreSQL 16 rejects with <em>&ldquo;unrecognized configuration parameter&rdquo;</em> and aborts the import.</li>
<li><strong><code>utf8mb4</code> on the instance</strong>: <code>--default-character-set=utf8mb4</code> in the dump does not fix an instance created with <code>latin1</code>; check <code>character_set_server</code> in the instance flags before importing.</li>
<li><strong>The DEFINER <code>sed</code> also touches data</strong>: if some row literally contains <code>DEFINER=`x`@`y` </code> (rare, but a CMS that stores SQL can), skip it with a pattern anchored to <code>CREATE</code>, or review the dump diff.</li>
</ul>
<h2 id="see-also">See also</h2>
<ul>
<li><a href="/en/posts/databases/postgresql-admin-cheat-sheet/">PostgreSQL: admin cheat sheet</a> — roles, <code>pg_dump</code>, <code>REASSIGN OWNED</code> and friends.</li>
<li><a href="https://docs.cloud.google.com/sql/docs/mysql/import-export/import-export-sql">Import and export using SQL dump files (MySQL)</a>, <a href="https://docs.cloud.google.com/sql/docs/mysql/import-export#definer-clause">DEFINER clause</a> and <a href="https://docs.cloud.google.com/sql/docs/postgres/import-export/import-export-dmp">pg_dump / pg_restore (PostgreSQL)</a> in the Cloud SQL documentation.</li>
</ul>
]]></content:encoded></item><item><title>Oracle CQN with Python: learn about table changes without polling (and why oracle_fdw fires no triggers)</title><link>https://inet.sh/en/posts/databases/oracle-cqn-python-without-polling/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/databases/oracle-cqn-python-without-polling/</guid><description>Continuous Query Notification makes Oracle notify your application when the rows of a query change, with no polling: GRANT CHANGE NOTIFICATION, a connection with events=True, subscribe() and registerquery() in python-oracledb. What it needs from the network (the database connects back to the client, or client_initiated=True), the ORA-29977 from SELECT *, and the real case: syncing Oracle into PostgreSQL, because oracle_fdw fires no triggers when the Oracle side changes.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">GRANT</span><span class="w"> </span><span class="n">CHANGE</span><span class="w"> </span><span class="n">NOTIFICATION</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">app_user</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">oracledb</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">on_change</span><span class="p">(</span><span class="n">message</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">query</span> <span class="ow">in</span> <span class="n">message</span><span class="o">.</span><span class="n">queries</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">table</span> <span class="ow">in</span> <span class="n">query</span><span class="o">.</span><span class="n">tables</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">table</span><span class="o">.</span><span class="n">rows</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="nb">print</span><span class="p">(</span><span class="n">table</span><span class="o">.</span><span class="n">name</span><span class="p">,</span> <span class="n">row</span><span class="o">.</span><span class="n">operation</span><span class="p">,</span> <span class="n">row</span><span class="o">.</span><span class="n">rowid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">conn</span> <span class="o">=</span> <span class="n">oracledb</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">user</span><span class="o">=</span><span class="s2">&#34;app_user&#34;</span><span class="p">,</span> <span class="n">password</span><span class="o">=</span><span class="n">pw</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">dsn</span><span class="o">=</span><span class="s2">&#34;db.example:1521/orclpdb1&#34;</span><span class="p">,</span> <span class="n">events</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">sub</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">callback</span><span class="o">=</span><span class="n">on_change</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                     <span class="n">operations</span><span class="o">=</span><span class="n">oracledb</span><span class="o">.</span><span class="n">OPCODE_INSERT</span> <span class="o">|</span> <span class="n">oracledb</span><span class="o">.</span><span class="n">OPCODE_UPDATE</span> <span class="o">|</span> <span class="n">oracledb</span><span class="o">.</span><span class="n">OPCODE_DELETE</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                     <span class="n">qos</span><span class="o">=</span><span class="n">oracledb</span><span class="o">.</span><span class="n">SUBSCR_QOS_QUERY</span> <span class="o">|</span> <span class="n">oracledb</span><span class="o">.</span><span class="n">SUBSCR_QOS_ROWIDS</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">sub</span><span class="o">.</span><span class="n">registerquery</span><span class="p">(</span><span class="s2">&#34;select id, status, updated_at from orders&#34;</span><span class="p">)</span>   <span class="c1"># explicit columns, never SELECT *</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">input</span><span class="p">(</span><span class="s2">&#34;Ctrl+C to exit</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Every <code>COMMIT</code> that changes the result of that query fires <code>on_change</code> with the affected <code>rowid</code>s. No polling, no triggers, no GoldenGate.</p>
<h2 id="why">Why</h2>
<p>When another system must react to changes in Oracle (invalidate a cache, replicate to another database, kick off a process), the usual approach is asking every N seconds with <code>WHERE updated_at &gt; :last</code>. It works until it does not: rows without a date column, deletes that leave no trace, useless load on the database.</p>
<p><strong>Continuous Query Notification</strong> flips the flow: you register a query and Oracle calls you when it changes. In <em>query</em> mode (<code>SUBSCR_QOS_QUERY</code>) it notifies only if the result of that query changes; in <em>object</em> mode (without that flag) it notifies on any change to the tables involved. <code>SUBSCR_QOS_ROWIDS</code> includes the <code>rowid</code>s in the notification, so the callback can read exactly those rows.</p>
<h2 id="the-case-replicating-oracle-into-postgresql">The case: replicating Oracle into PostgreSQL</h2>
<p><code>oracle_fdw</code> lets you query Oracle tables from PostgreSQL as if they were local, and it is tempting to think a trigger on the <em>foreign table</em> will fire when data changes in Oracle. It does not: triggers on a foreign table only fire for writes made <strong>from PostgreSQL</strong> through it. The <code>oracle_fdw</code> author says it plainly: there is no automatic way to keep a PostgreSQL table in sync with an Oracle table from the PostgreSQL side; Oracle has to trigger it.</p>
<p>With CQN, the small Python service is the missing piece:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">on_change</span><span class="p">(</span><span class="n">message</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">rowids</span> <span class="o">=</span> <span class="p">[</span><span class="n">row</span><span class="o">.</span><span class="n">rowid</span> <span class="k">for</span> <span class="n">q</span> <span class="ow">in</span> <span class="n">message</span><span class="o">.</span><span class="n">queries</span> <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">q</span><span class="o">.</span><span class="n">tables</span> <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">t</span><span class="o">.</span><span class="n">rows</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">conn</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span> <span class="k">as</span> <span class="n">cur</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">cur</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;select id, status, updated_at from orders where rowid in (</span><span class="si">%s</span><span class="s2">)&#34;</span>
</span></span><span class="line"><span class="cl">                    <span class="o">%</span> <span class="s2">&#34;,&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;&#39;</span><span class="si">{</span><span class="n">r</span><span class="si">}</span><span class="s2">&#39;&#34;</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">rowids</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="k">for</span> <span class="n">id_</span><span class="p">,</span> <span class="n">status</span><span class="p">,</span> <span class="n">updated_at</span> <span class="ow">in</span> <span class="n">cur</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">pg</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;insert into orders (id, status, updated_at) values (</span><span class="si">%s</span><span class="s2">, </span><span class="si">%s</span><span class="s2">, </span><span class="si">%s</span><span class="s2">) &#34;</span>
</span></span><span class="line"><span class="cl">                       <span class="s2">&#34;on conflict (id) do update set status = excluded.status, updated_at = excluded.updated_at&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                       <span class="p">(</span><span class="n">id_</span><span class="p">,</span> <span class="n">status</span><span class="p">,</span> <span class="n">updated_at</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">pg</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Deletes arrive with <code>row.operation &amp; oracledb.OPCODE_DELETE</code> and a <code>rowid</code> that no longer exists: delete in PostgreSQL by the key you stored against that <code>rowid</code>, or flag the row.</p>
<h2 id="requirements">Requirements</h2>
<ul>
<li><strong>Permission</strong>: <code>GRANT CHANGE NOTIFICATION TO app_user</code>, besides <code>SELECT</code> on the tables.</li>
<li><strong><code>events=True</code></strong> on the connection that subscribes; without it <code>subscribe()</code> fails.</li>
<li><strong>Network back-channel.</strong> By default <strong>the database opens a connection towards your application</strong> to deliver notifications: the client machine needs an IP reachable from the Oracle server and an open port (<code>subscribe(..., ip_address=…, port=…)</code> pins which). Behind NAT, inside a container, or with a firewall in between, notifications simply never arrive and nobody tells you.</li>
<li><strong><code>client_initiated=True</code></strong> removes that problem: your app opens the connection. It requires Thick mode (<code>oracledb.init_oracle_client()</code> with Instant Client 19.4 or later) and a 19.4+ database. In Thin mode, python-oracledb&rsquo;s default, CQN works but only in the classic mode.</li>
<li><strong><code>job_queue_processes</code></strong> greater than zero on the database: notifications are dispatched through the job queue.</li>
</ul>
<h2 id="traps">Traps</h2>
<ul>
<li><strong><code>ORA-29977: Unsupported column type for query registration in guaranteed mode</code></strong>: in <em>query</em> mode some column types are not allowed (LOBs, among others), and <code>SELECT *</code> drags them in. List the columns you need, or drop <code>SUBSCR_QOS_QUERY</code> and work in <em>object</em> mode.</li>
<li><strong>Notifications are not durable</strong>: if your service is down during a change, it does not get it. On startup, reconcile first (by <code>updated_at</code>, or in full) and subscribe afterwards. <code>timeout=0</code> on <code>subscribe</code> keeps the subscription until the connection dies.</li>
<li><strong>One subscription per live connection</strong>: the <code>events=True</code> connection must stay open; give it keepalives and reconnect with a re-subscribe if it drops.</li>
<li><strong>Heavy volume</strong> (thousands of commits per second) is not CQN territory: that is where redo-log CDC comes in (GoldenGate, or Debezium with its Oracle connector over LogMiner).</li>
<li><strong><code>SELECT</code> with non-deterministic functions</strong> or over complex views may not register in <em>query</em> mode; simplify the query or fall back to <em>object</em> mode.</li>
</ul>
<p>Reference: <a href="https://python-oracledb.readthedocs.io/en/latest/user_guide/cqn.html">Continuous Query Notification</a> in the python-oracledb documentation and the <a href="https://github.com/laurenz/oracle_fdw/issues/342">oracle_fdw discussion</a>.</p>
]]></content:encoded></item><item><title>pgbouncer: 'unsupported startup parameter: extra_float_digits' (and 'no such database')</title><link>https://inet.sh/en/posts/databases/pgbouncer-unsupported-startup-parameter/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/databases/pgbouncer-unsupported-startup-parameter/</guid><description>JDBC, DBeaver, DataGrip, Npgsql or psycopg connect straight to PostgreSQL but through pgbouncer they fail with &amp;#39;unsupported startup parameter: extra_float_digits&amp;#39; (or &amp;#39;options&amp;#39;). pgbouncer rejects session parameters it cannot track across clients; ignore_startup_parameters accepts and drops them. What is safe to ignore, what is not, track_extra_parameters, and the &amp;#39;pooler error: no such database&amp;#39; from the [databases] section.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="c1">; /etc/pgbouncer/pgbouncer.ini</span>
</span></span><span class="line"><span class="cl"><span class="k">[pgbouncer]</span>
</span></span><span class="line"><span class="cl"><span class="na">ignore_startup_parameters</span> <span class="o">=</span> <span class="s">extra_float_digits, options</span>
</span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">systemctl reload pgbouncer          <span class="c1"># or RELOAD; on the admin console: psql -p 6432 -U pgbouncer pgbouncer</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>extra_float_digits</code> can be ignored with no effect. <code>options</code> (what JDBC and DataGrip use for <code>-c search_path=…</code>) is ignored too, but then the client&rsquo;s <code>search_path</code> <strong>never arrives</strong>: pin it on the role with <code>alter role app set search_path = app, public</code>.</p>
<h2 id="the-error">The error</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">FATAL: unsupported startup parameter: extra_float_digits
</span></span></code></pre></td></tr></table>
</div>
</div><p>or, depending on the client, <code>pooler error: unsupported startup parameter: extra_float_digits</code>. Straight to port 5432 it connects; through pgbouncer&rsquo;s 6432, it does not. JDBC sends it (everything using the PostgreSQL Java driver: DBeaver, DataGrip, Spring, Jenkins), as do Npgsql and some ORMs; other clients send <code>options</code>.</p>
<h2 id="why-it-happens">Why it happens</h2>
<p>When a client opens a connection, it puts the session parameters it wants into the startup packet (<code>extra_float_digits=3</code>, <code>options=-c search_path=app</code>). pgbouncer, in <code>transaction</code> or <code>statement</code> mode, shares one server connection among many clients, so it only lets through the parameters it knows how to store per client and restore on the server when that client&rsquo;s turn comes: <code>client_encoding</code>, <code>datestyle</code>, <code>timezone</code>, <code>standard_conforming_strings</code>, <code>application_name</code>. Anything else would be a setting leaking from one client to another; rather than that, pgbouncer rejects the connection.</p>
<p>Two ways to resolve it:</p>
<table>
  <thead>
      <tr>
          <th>Option</th>
          <th>What it does</th>
          <th>For which parameters</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ignore_startup_parameters</code></td>
          <td>Accepts the parameter and drops it</td>
          <td>Those that change nothing (<code>extra_float_digits</code>) or that you can pin elsewhere (<code>options</code>)</td>
      </tr>
      <tr>
          <td><code>track_extra_parameters</code></td>
          <td>Stores it per client and restores it on the server, as it does with <code>timezone</code></td>
          <td>Parameters the server reports to the client (<code>ParameterStatus</code>) and that you truly need per session</td>
      </tr>
  </tbody>
</table>
<p><code>extra_float_digits</code> is harmless since PostgreSQL 12: <code>float</code> output is already the shortest exact representation and the parameter changes nothing. <code>options</code> with <code>search_path</code> <strong>does</strong> matter: ignored, the client sees the database&rsquo;s default <code>search_path</code> and unqualified queries fail with <code>relation does not exist</code>. The clean fix is the role:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">alter</span><span class="w"> </span><span class="k">role</span><span class="w"> </span><span class="n">app</span><span class="w"> </span><span class="k">set</span><span class="w"> </span><span class="n">search_path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">app</span><span class="p">,</span><span class="w"> </span><span class="k">public</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h2 id="no-such-database">&rsquo;no such database'</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">pooler error: no such database: app_db
</span></span></code></pre></td></tr></table>
</div>
</div><p>pgbouncer only routes databases listed under <code>[databases]</code>. Either add the entry, or a wildcard:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[databases]</span>
</span></span><span class="line"><span class="cl"><span class="na">app_db</span> <span class="o">=</span> <span class="s">host=db.example port=5432 dbname=app_db</span>
</span></span><span class="line"><span class="cl"><span class="na">*</span> <span class="o">=</span> <span class="s">host=db.example port=5432</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>With <code>*</code>, a client asking for <code>other_db</code> gets <code>host=db.example dbname=other_db</code>; those automatic entries clean themselves up after <code>autodb_idle_timeout</code>. And check the entry&rsquo;s <code>dbname=</code>: pointing <code>app_db</code> at another database through a copy-paste is common.</p>
<h2 id="traps">Traps</h2>
<ul>
<li><strong><code>RELOAD</code> is enough</strong> for <code>ignore_startup_parameters</code> and <code>[databases]</code>; <code>listen_*</code> and <code>auth_type</code> need a restart.</li>
<li><strong><code>transaction</code> mode and <code>SET</code></strong>: a manual <code>SET search_path</code> in the session is lost as soon as the server connection moves to another client; use <code>SET LOCAL</code> inside the transaction or the <code>alter role</code>.</li>
<li><strong>Named prepared statements</strong> (JDBC <code>prepareThreshold</code>, <code>PREPARE</code>) do not work in <code>transaction</code> mode on old pgbouncer; since 1.21, <code>max_prepared_statements = 200</code> supports them.</li>
<li><strong>SCRAM</strong>: with <code>auth_type = scram-sha-256</code> the <code>userlist.txt</code> needs the SCRAM hash exactly as it comes out of <code>pg_authid</code>, or <code>auth_query</code> so pgbouncer looks it up.</li>
<li><code>SHOW CONFIG;</code> and <code>SHOW DATABASES;</code> on the admin console tell you what is really loaded, not what you think you wrote.</li>
</ul>
<p>See also: <a href="/en/posts/databases/postgresql-admin-cheat-sheet/">PostgreSQL: admin cheat sheet</a>. Reference: <a href="https://www.pgbouncer.org/config.html">pgbouncer.ini</a>.</p>
]]></content:encoded></item><item><title>PostgreSQL: admin cheat sheet (rows per table, sizes, sessions, read-only user)</title><link>https://inet.sh/en/posts/databases/postgresql-admin-cheat-sheet/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/en/posts/databases/postgresql-admin-cheat-sheet/</guid><description>The queries a sysadmin keeps repeating on PostgreSQL: count the rows of every table in a single query (query_to_xml + xpath) or estimate them instantly (n_live_tup), database and table sizes, who is connected and how to kick them out, a read-only user with pg_read_all_data (14+) or with GRANT and ALTER DEFAULT PRIVILEGES, timezone per database or role, and the psql flags that avoid surprises.</description><content:encoded><![CDATA[<h2 id="tldr">TL;DR</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- Exact rows of every table in a schema, in a single query
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">select</span><span class="w"> </span><span class="n">table_schema</span><span class="p">,</span><span class="w"> </span><span class="k">table_name</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="p">(</span><span class="n">xpath</span><span class="p">(</span><span class="s1">&#39;/row/cnt/text()&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">xml_count</span><span class="p">))[</span><span class="mi">1</span><span class="p">]::</span><span class="nb">text</span><span class="p">::</span><span class="nb">bigint</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="k">row_count</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">from</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">select</span><span class="w"> </span><span class="n">table_schema</span><span class="p">,</span><span class="w"> </span><span class="k">table_name</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">         </span><span class="n">query_to_xml</span><span class="p">(</span><span class="n">format</span><span class="p">(</span><span class="s1">&#39;select count(*) as cnt from %I.%I&#39;</span><span class="p">,</span><span class="w"> </span><span class="n">table_schema</span><span class="p">,</span><span class="w"> </span><span class="k">table_name</span><span class="p">),</span><span class="w"> </span><span class="k">false</span><span class="p">,</span><span class="w"> </span><span class="k">true</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;&#39;</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">xml_count</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">from</span><span class="w"> </span><span class="n">information_schema</span><span class="p">.</span><span class="n">tables</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="k">where</span><span class="w"> </span><span class="n">table_schema</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;public&#39;</span><span class="w"> </span><span class="k">and</span><span class="w"> </span><span class="n">table_type</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;BASE TABLE&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="n">t</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">order</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="k">row_count</span><span class="w"> </span><span class="k">desc</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Instant estimate (does not read the tables)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">select</span><span class="w"> </span><span class="n">relname</span><span class="p">,</span><span class="w"> </span><span class="n">n_live_tup</span><span class="w"> </span><span class="k">from</span><span class="w"> </span><span class="n">pg_stat_user_tables</span><span class="w"> </span><span class="k">order</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="n">n_live_tup</span><span class="w"> </span><span class="k">desc</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Read-only user (PostgreSQL 14 or later)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">create</span><span class="w"> </span><span class="k">user</span><span class="w"> </span><span class="n">reporting</span><span class="w"> </span><span class="k">with</span><span class="w"> </span><span class="n">password</span><span class="w"> </span><span class="s1">&#39;…&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">grant</span><span class="w"> </span><span class="n">pg_read_all_data</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="n">reporting</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h2 id="counting-rows">Counting rows</h2>
<p>An exact <code>count(*)</code> on each table means scanning it whole; on a database of hundreds of GB it takes a while. What you almost always want is the <strong>estimate</strong> autovacuum maintains, refreshed on every <code>ANALYZE</code> and usually within a couple of points of the real number:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">select</span><span class="w"> </span><span class="n">schemaname</span><span class="p">,</span><span class="w"> </span><span class="n">relname</span><span class="p">,</span><span class="w"> </span><span class="n">n_live_tup</span><span class="p">,</span><span class="w"> </span><span class="n">n_dead_tup</span><span class="p">,</span><span class="w"> </span><span class="n">last_autoanalyze</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">from</span><span class="w"> </span><span class="n">pg_stat_user_tables</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">order</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="n">n_live_tup</span><span class="w"> </span><span class="k">desc</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>When you need the exact number for <strong>every</strong> table without writing a <code>DO</code> block in PL/pgSQL, the <code>query_to_xml</code> trick: it runs a dynamic <code>count(*)</code> per table, returns the result as XML, and <code>xpath</code> extracts the number. It is plain SQL, works from any client, and can be filtered by schema or name. Just remember it runs the counts serially: on big tables, patience.</p>
<h2 id="sizes">Sizes</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">select</span><span class="w"> </span><span class="n">pg_size_pretty</span><span class="p">(</span><span class="n">pg_database_size</span><span class="p">(</span><span class="n">current_database</span><span class="p">()));</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">select</span><span class="w"> </span><span class="n">n</span><span class="p">.</span><span class="n">nspname</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="k">schema</span><span class="p">,</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">relname</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="k">table</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">       </span><span class="n">pg_size_pretty</span><span class="p">(</span><span class="n">pg_total_relation_size</span><span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="n">oid</span><span class="p">))</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">total</span><span class="p">,</span><span class="w">   </span><span class="c1">-- data + indexes + TOAST
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="w">       </span><span class="n">pg_size_pretty</span><span class="p">(</span><span class="n">pg_relation_size</span><span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="n">oid</span><span class="p">))</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="k">data</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">from</span><span class="w"> </span><span class="n">pg_class</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="k">join</span><span class="w"> </span><span class="n">pg_namespace</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="k">on</span><span class="w"> </span><span class="n">n</span><span class="p">.</span><span class="n">oid</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">relnamespace</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">where</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">relkind</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;r&#39;</span><span class="w"> </span><span class="k">and</span><span class="w"> </span><span class="n">n</span><span class="p">.</span><span class="n">nspname</span><span class="w"> </span><span class="k">not</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;pg_catalog&#39;</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;information_schema&#39;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">order</span><span class="w"> </span><span class="k">by</span><span class="w"> </span><span class="n">pg_total_relation_size</span><span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="n">oid</span><span class="p">)</span><span class="w"> </span><span class="k">desc</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">limit</span><span class="w"> </span><span class="mi">20</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h2 id="sessions">Sessions</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="c1">-- Who is connected and what they are doing
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">select</span><span class="w"> </span><span class="n">pid</span><span class="p">,</span><span class="w"> </span><span class="n">usename</span><span class="p">,</span><span class="w"> </span><span class="k">state</span><span class="p">,</span><span class="w"> </span><span class="n">client_addr</span><span class="p">,</span><span class="w"> </span><span class="n">application_name</span><span class="p">,</span><span class="w"> </span><span class="k">left</span><span class="p">(</span><span class="n">query</span><span class="p">,</span><span class="w"> </span><span class="mi">80</span><span class="p">)</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">query</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">from</span><span class="w"> </span><span class="n">pg_stat_activity</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">where</span><span class="w"> </span><span class="n">datname</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">current_database</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Cancel a query (the session stays)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">select</span><span class="w"> </span><span class="n">pg_cancel_backend</span><span class="p">(</span><span class="mi">12345</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Kick everyone but yourself (before a DROP or a restore)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">select</span><span class="w"> </span><span class="n">pg_terminate_backend</span><span class="p">(</span><span class="n">pid</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">from</span><span class="w"> </span><span class="n">pg_stat_activity</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">where</span><span class="w"> </span><span class="n">datname</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;app_db&#39;</span><span class="w"> </span><span class="k">and</span><span class="w"> </span><span class="n">pid</span><span class="w"> </span><span class="o">&lt;&gt;</span><span class="w"> </span><span class="n">pg_backend_pid</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Or, since PostgreSQL 13, directly:
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">drop</span><span class="w"> </span><span class="k">database</span><span class="w"> </span><span class="n">app_db</span><span class="w"> </span><span class="k">with</span><span class="w"> </span><span class="p">(</span><span class="k">force</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Uptime
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">select</span><span class="w"> </span><span class="n">date_trunc</span><span class="p">(</span><span class="s1">&#39;second&#39;</span><span class="p">,</span><span class="w"> </span><span class="k">current_timestamp</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">pg_postmaster_start_time</span><span class="p">())</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="n">uptime</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h2 id="read-only-user">Read-only user</h2>
<p>On <strong>14 or later</strong>, a predefined role solves it for every schema, including tables created tomorrow:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">create</span><span class="w"> </span><span class="k">user</span><span class="w"> </span><span class="n">reporting</span><span class="w"> </span><span class="k">with</span><span class="w"> </span><span class="n">password</span><span class="w"> </span><span class="s1">&#39;…&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">grant</span><span class="w"> </span><span class="n">pg_read_all_data</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="n">reporting</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>On earlier versions, or if you want to limit it to one schema, the classic path:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">grant</span><span class="w"> </span><span class="k">connect</span><span class="w"> </span><span class="k">on</span><span class="w"> </span><span class="k">database</span><span class="w"> </span><span class="n">app_db</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="n">reporting</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">grant</span><span class="w"> </span><span class="k">usage</span><span class="w"> </span><span class="k">on</span><span class="w"> </span><span class="k">schema</span><span class="w"> </span><span class="k">public</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="n">reporting</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">grant</span><span class="w"> </span><span class="k">select</span><span class="w"> </span><span class="k">on</span><span class="w"> </span><span class="k">all</span><span class="w"> </span><span class="n">tables</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="k">schema</span><span class="w"> </span><span class="k">public</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="n">reporting</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- future tables: only covers those created by the role that runs this (or FOR ROLE app_owner)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">alter</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="k">privileges</span><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="k">role</span><span class="w"> </span><span class="n">app_owner</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="k">schema</span><span class="w"> </span><span class="k">public</span><span class="w"> </span><span class="k">grant</span><span class="w"> </span><span class="k">select</span><span class="w"> </span><span class="k">on</span><span class="w"> </span><span class="n">tables</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="n">reporting</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">-- Undo
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">revoke</span><span class="w"> </span><span class="k">all</span><span class="w"> </span><span class="k">on</span><span class="w"> </span><span class="k">all</span><span class="w"> </span><span class="n">tables</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="k">schema</span><span class="w"> </span><span class="k">public</span><span class="w"> </span><span class="k">from</span><span class="w"> </span><span class="n">reporting</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">alter</span><span class="w"> </span><span class="k">default</span><span class="w"> </span><span class="k">privileges</span><span class="w"> </span><span class="k">for</span><span class="w"> </span><span class="k">role</span><span class="w"> </span><span class="n">app_owner</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="k">schema</span><span class="w"> </span><span class="k">public</span><span class="w"> </span><span class="k">revoke</span><span class="w"> </span><span class="k">select</span><span class="w"> </span><span class="k">on</span><span class="w"> </span><span class="n">tables</span><span class="w"> </span><span class="k">from</span><span class="w"> </span><span class="n">reporting</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p><code>alter default privileges</code> does not touch what already exists and only applies to objects created by the given role: if another account creates tables, <code>reporting</code> will not see them. Hence the value of <code>pg_read_all_data</code>.</p>
<h2 id="passwords-timezone-and-psql">Passwords, timezone, and psql</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="err">\</span><span class="n">password</span><span class="w"> </span><span class="n">app_user</span><span class="w">                                  </span><span class="c1">-- from psql, without leaving the password in the history
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">alter</span><span class="w"> </span><span class="k">user</span><span class="w"> </span><span class="n">app_user</span><span class="w"> </span><span class="k">with</span><span class="w"> </span><span class="n">password</span><span class="w"> </span><span class="s1">&#39;…&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">alter</span><span class="w"> </span><span class="k">database</span><span class="w"> </span><span class="n">app_db</span><span class="w"> </span><span class="k">set</span><span class="w"> </span><span class="n">timezone</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="s1">&#39;America/Mexico_City&#39;</span><span class="p">;</span><span class="w">   </span><span class="c1">-- new connections to that database
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">alter</span><span class="w"> </span><span class="k">role</span><span class="w"> </span><span class="n">reporting</span><span class="w"> </span><span class="k">set</span><span class="w"> </span><span class="n">timezone</span><span class="w"> </span><span class="k">to</span><span class="w"> </span><span class="s1">&#39;UTC&#39;</span><span class="p">;</span><span class="w">                    </span><span class="c1">-- new sessions of that role
</span></span></span></code></pre></td></tr></table>
</div>
</div><div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">psql -v <span class="nv">ON_ERROR_STOP</span><span class="o">=</span><span class="m">1</span> -f data.sql app_db      <span class="c1"># a script that stops at the first error</span>
</span></span><span class="line"><span class="cl">psql -P <span class="nv">pager</span><span class="o">=</span>off app_db                        <span class="c1"># no pager (or \pset pager 0 inside)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>And inside <code>psql</code>, <code>\x auto</code> (vertical columns when they do not fit) and <code>\timing</code>.</p>
<h2 id="minimal-maintenance">Minimal maintenance</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">vacuum</span><span class="w"> </span><span class="p">(</span><span class="k">verbose</span><span class="p">,</span><span class="w"> </span><span class="k">analyze</span><span class="p">)</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">reindex</span><span class="w"> </span><span class="k">table</span><span class="w"> </span><span class="n">concurrently</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span><span class="w">               </span><span class="c1">-- 12+, without blocking writes
</span></span></span></code></pre></td></tr></table>
</div>
</div><p><code>VACUUM FULL</code> rewrites the table and locks it meanwhile; to reclaim space in production, <code>pg_repack</code>.</p>
<h2 id="traps">Traps</h2>
<ul>
<li><strong><code>n_live_tup</code> is an estimate</strong>: freshly analyzed it is faithful; on a table with heavy churn it can drift until the next autoanalyze.</li>
<li><strong><code>pg_read_all_data</code> does not bypass RLS</strong> and grants no DDL; it is reading tables, views, and sequences, nothing else.</li>
<li><strong><code>pg_terminate_backend</code> cuts transactions</strong>: the application sees an error and reconnects. To only abort a query, <code>pg_cancel_backend</code>.</li>
<li><strong><code>alter role … set timezone</code></strong> applies to new sessions; the one already open stays the same.</li>
<li><strong><code>quote_ident</code>/<code>format('%I')</code></strong> whenever you build SQL with table names: a name with capitals or dashes breaks the query without them.</li>
</ul>
<h2 id="see-also">See also</h2>
<ul>
<li><a href="/en/posts/databases/pgbouncer-unsupported-startup-parameter/">pgbouncer: unsupported startup parameter</a></li>
<li><a href="/en/posts/gcp/cloud-sql-import-dumps-definer-gtid-extensions/">Cloud SQL: importing dumps without DEFINER, GTID or extension errors</a></li>
</ul>
]]></content:encoded></item></channel></rss>