<?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>Databases on inetshell</title><link>https://inet.sh/en/posts/databases/</link><description>Recent content in Databases 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/posts/databases/index.xml" rel="self" type="application/rss+xml"/><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>