<?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/es/tags/postgresql/</link><description>Recent content in Postgresql on inetshell</description><generator>Hugo</generator><language>es</language><lastBuildDate>Sat, 26 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://inet.sh/es/tags/postgresql/index.xml" rel="self" type="application/rss+xml"/><item><title>Importar dumps a Cloud SQL sin errores: DEFINER y SUPER (ERROR 1227) en MySQL, extensiones y owners en PostgreSQL</title><link>https://inet.sh/es/posts/gcp/cloud-sql-importar-dumps-definer-gtid-extensiones/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/es/posts/gcp/cloud-sql-importar-dumps-definer-gtid-extensiones/</guid><description>Un dump de mysqldump o pg_dump que funciona en cualquier servidor falla al importarlo en Cloud SQL: ERROR 1227 &amp;#39;you need the SUPER privilege&amp;#39; por los DEFINER y por SET @@GLOBAL.GTID_PURGED, &amp;#39;must be owner of extension plpgsql&amp;#39; y &amp;#39;role does not exist&amp;#39; en PostgreSQL. Los flags de exportación que Cloud SQL espera, qué hacer con los DEFINER (crear los usuarios, quitarlos o pasar a INVOKER), el sed que comenta los EXTENSION, el permiso del bucket y la importación con gcloud sql import sql.</description><content:encoded><![CDATA[<h2 id="en-corto">En corto</h2>
<p><strong>MySQL</strong>, en el servidor origen:</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>, en el servidor origen:</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>Y en los dos casos:</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> acepta SQL plano o <code>.gz</code>: el formato <em>custom</em> de <code>pg_dump</code> va por <code>pg_restore</code> (abajo).</p>
<h2 id="los-errores-y-de-dónde-salen">Los errores y de dónde salen</h2>
<p>En Cloud SQL nadie tiene <code>SUPER</code> (MySQL) ni es superusuario (PostgreSQL); el administrador es <code>root@%</code> o <code>postgres</code>, miembro de <code>cloudsqlsuperuser</code>. Todo lo que un dump normal da por hecho de un servidor propio se rompe justo ahí:</p>
<table>
  <thead>
      <tr>
          <th>Error al importar</th>
          <th>Motor</th>
          <th>Causa</th>
          <th>Arreglo</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> (línea de un <code>CREATE … VIEW</code>, <code>TRIGGER</code>, <code>PROCEDURE</code>, <code>FUNCTION</code> o <code>EVENT</code>)</td>
          <td>MySQL</td>
          <td><code>DEFINER=`user`@`host` </code> de un usuario que no existe en la instancia, o de <code>root@localhost</code>, que en Cloud SQL nunca existe. El dump no trae usuarios (y no debe traerlos)</td>
          <td>Crear esos usuarios antes de importar, o quitar los <code>DEFINER=</code> con el <code>sed</code></td>
      </tr>
      <tr>
          <td>El mismo <code>ERROR 1227</code> en las primeras líneas, en <code>SET @@GLOBAL.GTID_PURGED=…</code></td>
          <td>MySQL</td>
          <td>El origen tiene GTID y <code>mysqldump</code> escribe la posición, que solo <code>SUPER</code> puede fijar</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> al <strong>usar</strong> la vista, no al importar</td>
          <td>MySQL</td>
          <td>Dejaste un <code>DEFINER</code> de un usuario que no existe en la instancia</td>
          <td>Quitar el <code>DEFINER</code> o crear el usuario con ese host exacto</td>
      </tr>
      <tr>
          <td><code>ERROR: must be owner of extension plpgsql</code></td>
          <td>PostgreSQL</td>
          <td><code>COMMENT ON EXTENSION plpgsql</code> del dump: en Cloud SQL las extensiones pertenecen a <code>cloudsqladmin</code></td>
          <td>Comentar las líneas <code>EXTENSION</code> con el <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> y <code>GRANT … TO</code> para roles del servidor viejo</td>
          <td><code>--no-owner --no-acl</code>, o crear los roles antes</td>
      </tr>
      <tr>
          <td><code>ERROR: permission denied for schema public</code> o <code>must be owner of …</code> a mitad del dump</td>
          <td>PostgreSQL</td>
          <td>Importaste con un <code>--user</code> que no es el dueño de lo que ya existe en la base</td>
          <td>Importa como <code>postgres</code> en una base vacía; reasigna después</td>
      </tr>
  </tbody>
</table>
<h2 id="mysql-qué-hacer-con-los-definer">MySQL: qué hacer con los DEFINER</h2>
<p>Primero mira cuántos y cuáles hay en el origen:</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>Tres salidas, de más a menos fiel al original:</p>
<ol>
<li><strong>Crear los usuarios en la instancia antes de importar</strong>, con el mismo <code>user@host</code>: <code>gcloud sql users create app --host=% --instance=my-instance --password=…</code>. Es lo que recomienda la documentación, y mantiene las vistas y procedimientos exactamente como estaban. <code>root@localhost</code> es la excepción: no se puede crear en Cloud SQL, así que un <code>DEFINER</code> con ese valor hay que cambiarlo sí o sí.</li>
<li><strong>Quitar el <code>DEFINER</code></strong> con el <code>sed</code> de arriba: los objetos se crean con el usuario que importa como definer. Suficiente cuando los usuarios del origen no van a existir en Cloud SQL.</li>
<li><strong>Pasar a <code>SQL SECURITY INVOKER</code></strong> (<code>sed -E 's/SQL SECURITY DEFINER/SQL SECURITY INVOKER/g'</code> además del anterior): la vista o el procedimiento corre con los privilegios de quien lo llama y el definer deja de importar. Cambia el modelo de permisos; revisa que los usuarios de la aplicación puedan leer las tablas de debajo.</li>
</ol>
<p>El <code>sed</code> es deliberadamente amplio porque <code>DEFINER=</code> aparece también dentro de comentarios condicionales (<code>/*!50013 DEFINER=… SQL SECURITY DEFINER */</code>); borrar solo el <code>DEFINER=</code> deja el resto intacto y válido.</p>
<h2 id="mysql-los-flags-uno-por-uno">MySQL: los flags, uno por uno</h2>
<table>
  <thead>
      <tr>
          <th>Flag</th>
          <th>Por qué</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>--hex-blob</code></td>
          <td>Columnas binarias en hexadecimal; sin él, un <code>BLOB</code> con bytes raros se corrompe al pasar por el juego de caracteres</td>
      </tr>
      <tr>
          <td><code>--single-transaction</code></td>
          <td>Snapshot consistente de InnoDB sin bloquear tablas; imprescindible en producción</td>
      </tr>
      <tr>
          <td><code>--set-gtid-purged=OFF</code></td>
          <td>No escribir <code>SET @@GLOBAL.GTID_PURGED</code>, que Cloud SQL no deja ejecutar</td>
      </tr>
      <tr>
          <td><code>--default-character-set=utf8mb4</code></td>
          <td>Que el dump y el <code>SET NAMES</code> que trae coincidan con lo que hay en las tablas; evita el clásico <code>Ã©</code></td>
      </tr>
      <tr>
          <td><code>--databases app</code></td>
          <td>Obligatorio según la documentación, y nunca con la base <code>mysql</code>. Añade <code>CREATE DATABASE IF NOT EXISTS</code> y <code>USE app</code>, así <code>--database</code> en la importación es opcional</td>
      </tr>
      <tr>
          <td><code>--routines --triggers --events</code></td>
          <td><code>mysqldump</code> solo incluye triggers por defecto; procedimientos, funciones y eventos hay que pedirlos. Todos llevan <code>DEFINER</code></td>
      </tr>
  </tbody>
</table>
<p>Si el usuario con el que haces el dump no tiene <code>PROCESS</code>, <code>mysqldump</code> 8.0.21+ falla al inicio con <em>&ldquo;Access denied; you need (at least one of) the PROCESS privilege(s)&rdquo;</em> al consultar tablespaces: añade <code>--no-tablespaces</code>.</p>
<h2 id="postgresql-plano-o-custom">PostgreSQL: plano o custom</h2>
<p><code>--no-owner --no-acl</code> hacen que todo lo importado pertenezca al <code>--user</code> y no haya <code>GRANT</code> a roles fantasma. Si la aplicación se conecta con otro rol, crea el rol antes y usa <code>--user=app</code>, o reasigna después:</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">-- conectado como postgres, en la base importada
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Las extensiones que necesite la base (<code>pg_trgm</code>, <code>postgis</code>, <code>uuid-ossp</code>…) créalas <strong>antes</strong> de importar, como <code>postgres</code>, en la base destino: <code>CREATE EXTENSION IF NOT EXISTS pg_trgm;</code>. Solo funcionan las de la lista de extensiones soportadas de Cloud SQL; <code>plpgsql</code> ya está. Con eso, comentar los <code>CREATE EXTENSION</code> del dump no quita nada.</p>
<p>Si prefieres el formato <em>custom</em> (<code>-Fc</code>), el filtro de extensiones va en la lista de contenidos de <code>pg_restore</code>, tal como lo documenta Google, y restauras a través del 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> restaura en paralelo, cosa que la importación desde el bucket no hace.</p>
<h2 id="trampas">Trampas</h2>
<ul>
<li><strong>El rol del bucket va a la service account de la instancia</strong>, no a la tuya: la documentación pide <code>storage.objectAdmin</code> (para importar bastan <code>storage.objects.get</code> y <code>list</code>, es decir, <code>objectViewer</code>). Sin él, <em>&ldquo;The service account does not have the required permissions for the bucket&rdquo;</em> aunque tú puedas leer el objeto.</li>
<li><strong>Una operación a la vez por instancia</strong>: si hay un backup o export en marcha, la importación falla con <em>&ldquo;operation already in progress&rdquo;</em>. <code>gcloud sql operations list --instance=my-instance</code> lo muestra.</li>
<li><strong>Dumps grandes</strong>: la importación es en serie y tarda; lanza con <code>--async</code> y vigila con <code>gcloud sql operations wait OPERATION_ID --timeout=unlimited</code>, o el <code>gcloud</code> se te muere por el camino.</li>
<li><strong>Versión de <code>pg_dump</code></strong>: usa la misma versión mayor que el destino o inferior. <code>pg_dump</code> 17 escribe <code>SET transaction_timeout = 0;</code>, que un PostgreSQL 16 rechaza con <em>&ldquo;unrecognized configuration parameter&rdquo;</em> y aborta la importación.</li>
<li><strong><code>utf8mb4</code> en la instancia</strong>: <code>--default-character-set=utf8mb4</code> en el dump no arregla una instancia creada con <code>latin1</code>; comprueba <code>character_set_server</code> en los flags de la instancia antes de importar.</li>
<li><strong>El <code>sed</code> de DEFINER también toca datos</strong>: si alguna fila contiene literalmente <code>DEFINER=`x`@`y` </code> (raro, pero un CMS que guarda SQL puede), sáltala con un patrón anclado a <code>CREATE</code>, o revisa el diff del dump.</li>
</ul>
<h2 id="ver-también">Ver también</h2>
<ul>
<li><a href="/es/posts/databases/postgresql-cheat-sheet-administracion/">PostgreSQL: cheat sheet de administración</a> — roles, <code>pg_dump</code>, <code>REASSIGN OWNED</code> y compañía.</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> y <a href="https://docs.cloud.google.com/sql/docs/postgres/import-export/import-export-dmp">pg_dump / pg_restore (PostgreSQL)</a> en la documentación de Cloud SQL.</li>
</ul>
]]></content:encoded></item><item><title>Oracle CQN con Python: enterarte de los cambios de una tabla sin polling (y por qué oracle_fdw no dispara triggers)</title><link>https://inet.sh/es/posts/databases/oracle-cqn-python-sin-polling/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/es/posts/databases/oracle-cqn-python-sin-polling/</guid><description>Continuous Query Notification hace que Oracle avise a tu aplicación cuando cambian las filas de una consulta, sin polling: GRANT CHANGE NOTIFICATION, conexión con events=True, subscribe() y registerquery() en python-oracledb. Qué exige de red (la base conecta de vuelta al cliente, o client_initiated=True), el ORA-29977 del SELECT *, y el caso real: sincronizar Oracle hacia PostgreSQL, porque oracle_fdw no dispara triggers cuando cambia el lado Oracle.</description><content:encoded><![CDATA[<h2 id="en-corto">En corto</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"># columnas explícitas, nunca 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 para salir</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>Cada <code>COMMIT</code> que cambie el resultado de esa consulta dispara <code>on_change</code> con los <code>rowid</code> afectados. Sin polling, sin triggers, sin GoldenGate.</p>
<h2 id="por-qué">Por qué</h2>
<p>Cuando otro sistema necesita reaccionar a cambios en Oracle (invalidar una caché, replicar hacia otra base, disparar un proceso), lo habitual es preguntar cada N segundos con <code>WHERE updated_at &gt; :last</code>. Funciona hasta que no: filas sin columna de fecha, borrados que no dejan rastro, carga inútil sobre la base.</p>
<p><strong>Continuous Query Notification</strong> invierte el flujo: registras una consulta y Oracle te llama cuando cambia. En modo <em>query</em> (<code>SUBSCR_QOS_QUERY</code>) avisa solo si cambia el resultado de esa consulta; en modo <em>object</em> (sin ese flag) avisa ante cualquier cambio en las tablas implicadas. <code>SUBSCR_QOS_ROWIDS</code> incluye los <code>rowid</code> en la notificación, con lo que el callback puede leer exactamente esas filas.</p>
<h2 id="el-caso-replicar-oracle-a-postgresql">El caso: replicar Oracle a PostgreSQL</h2>
<p><code>oracle_fdw</code> deja consultar tablas de Oracle desde PostgreSQL como si fueran locales, y es tentador pensar que un trigger sobre la <em>foreign table</em> se disparará cuando cambien los datos en Oracle. No: los triggers de una foreign table solo saltan con escrituras hechas <strong>desde PostgreSQL</strong> a través de ella. El autor de <code>oracle_fdw</code> lo dice tal cual: no hay forma automática de mantener sincronizada una tabla de PostgreSQL con una de Oracle desde el lado PostgreSQL; eso tiene que dispararlo Oracle.</p>
<p>Con CQN el pequeño servicio en Python es la pieza que falta:</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>Los borrados llegan con <code>row.operation &amp; oracledb.OPCODE_DELETE</code> y un <code>rowid</code> que ya no existe: borra en PostgreSQL por la clave que guardaste asociada a ese <code>rowid</code>, o marca la fila.</p>
<h2 id="requisitos">Requisitos</h2>
<ul>
<li><strong>Permiso</strong>: <code>GRANT CHANGE NOTIFICATION TO app_user</code>, además de <code>SELECT</code> sobre las tablas.</li>
<li><strong><code>events=True</code></strong> en la conexión que se suscribe; sin eso <code>subscribe()</code> falla.</li>
<li><strong>Red de vuelta.</strong> Por defecto es <strong>la base la que abre una conexión hacia tu aplicación</strong> para entregar las notificaciones: la máquina del cliente necesita una IP alcanzable desde el servidor Oracle y un puerto abierto (<code>subscribe(..., ip_address=…, port=…)</code> fija cuáles). Detrás de NAT, en un contenedor o con firewall entre medias, las notificaciones simplemente no llegan y nadie avisa.</li>
<li><strong><code>client_initiated=True</code></strong> elimina ese problema: la conexión la abre tu app. Requiere modo Thick (<code>oracledb.init_oracle_client()</code> con Instant Client 19.4 o superior) y base 19.4 o superior. En modo Thin, el por defecto de python-oracledb, CQN funciona pero solo en el modo clásico.</li>
<li><strong><code>job_queue_processes</code></strong> mayor que cero en la base: las notificaciones se despachan por el job queue.</li>
</ul>
<h2 id="trampas">Trampas</h2>
<ul>
<li><strong><code>ORA-29977: Unsupported column type for query registration in guaranteed mode</code></strong>: en modo <em>query</em> hay tipos de columna que Oracle no admite (LOB, entre otros), y <code>SELECT *</code> los arrastra. Lista las columnas que necesitas o quita <code>SUBSCR_QOS_QUERY</code> y trabaja en modo <em>object</em>.</li>
<li><strong>Las notificaciones no son durables</strong>: si tu servicio está caído durante un cambio, no lo recibe. Al arrancar, haz una reconciliación (por <code>updated_at</code>, o completa) y después suscríbete. <code>timeout=0</code> en <code>subscribe</code> mantiene la suscripción hasta que la conexión muera.</li>
<li><strong>Una suscripción por conexión viva</strong>: la conexión con <code>events=True</code> tiene que seguir abierta; ponle keepalives y reconecta con re-suscripción si se cae.</li>
<li><strong>Mucho volumen</strong> (miles de commits por segundo) no es territorio de CQN: ahí entra un CDC por redo log (GoldenGate, o Debezium con su conector de Oracle vía LogMiner).</li>
<li><strong><code>SELECT</code> con funciones no deterministas</strong> o sobre vistas complejas puede no registrarse en modo <em>query</em>; simplifica la consulta o cae a modo <em>object</em>.</li>
</ul>
<p>Referencia: <a href="https://python-oracledb.readthedocs.io/en/latest/user_guide/cqn.html">Continuous Query Notification</a> en la documentación de python-oracledb y la <a href="https://github.com/laurenz/oracle_fdw/issues/342">discusión en oracle_fdw</a>.</p>
]]></content:encoded></item><item><title>pgbouncer: 'unsupported startup parameter: extra_float_digits' (y 'no such database')</title><link>https://inet.sh/es/posts/databases/pgbouncer-unsupported-startup-parameter/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/es/posts/databases/pgbouncer-unsupported-startup-parameter/</guid><description>JDBC, DBeaver, DataGrip, Npgsql o psycopg conectan directo a PostgreSQL pero a través de pgbouncer fallan con &amp;#39;unsupported startup parameter: extra_float_digits&amp;#39; (o &amp;#39;options&amp;#39;). pgbouncer rechaza los parámetros de sesión que no puede rastrear entre clientes; ignore_startup_parameters los acepta y descarta. Qué es seguro ignorar, qué no, track_extra_parameters, y el &amp;#39;pooler error: no such database&amp;#39; de la sección [databases].</description><content:encoded><![CDATA[<h2 id="en-corto">En corto</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"># o RELOAD; en la consola de admin: psql -p 6432 -U pgbouncer pgbouncer</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>extra_float_digits</code> se puede ignorar sin efectos. <code>options</code> (lo que JDBC y DataGrip usan para <code>-c search_path=…</code>) también se ignora, pero entonces el <code>search_path</code> del cliente <strong>no llega</strong>: fíjalo en el rol con <code>alter role app set search_path = app, public</code>.</p>
<h2 id="el-error">El 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>o, según el cliente, <code>pooler error: unsupported startup parameter: extra_float_digits</code>. Directo al puerto 5432 conecta; por el 6432 de pgbouncer, no. Lo mandan JDBC (todo lo que use el driver de PostgreSQL para Java: DBeaver, DataGrip, Spring, Jenkins), Npgsql y algunos ORMs; otros clientes mandan <code>options</code>.</p>
<h2 id="por-qué-pasa">Por qué pasa</h2>
<p>Cuando un cliente abre conexión, mete en el paquete de inicio los parámetros de sesión que quiere (<code>extra_float_digits=3</code>, <code>options=-c search_path=app</code>). pgbouncer, en modo <code>transaction</code> o <code>statement</code>, comparte una misma conexión de servidor entre muchos clientes, así que solo deja pasar los parámetros que sabe guardar por cliente y restaurar en el servidor cuando le toca a ese cliente: <code>client_encoding</code>, <code>datestyle</code>, <code>timezone</code>, <code>standard_conforming_strings</code>, <code>application_name</code>. Cualquier otro sería un ajuste que se filtra de un cliente a otro; antes que eso, pgbouncer rechaza la conexión.</p>
<p>Dos formas de resolverlo:</p>
<table>
  <thead>
      <tr>
          <th>Opción</th>
          <th>Qué hace</th>
          <th>Para qué parámetros</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ignore_startup_parameters</code></td>
          <td>Acepta el parámetro y lo tira</td>
          <td>Los que no cambian nada (<code>extra_float_digits</code>) o los que puedes fijar en otro sitio (<code>options</code>)</td>
      </tr>
      <tr>
          <td><code>track_extra_parameters</code></td>
          <td>Lo guarda por cliente y lo restaura en el servidor, como hace con <code>timezone</code></td>
          <td>Parámetros que el servidor reporta al cliente (<code>ParameterStatus</code>) y que de verdad necesitas por sesión</td>
      </tr>
  </tbody>
</table>
<p><code>extra_float_digits</code> es inofensivo desde PostgreSQL 12: la salida de <code>float</code> ya es la representación más corta exacta y el parámetro no cambia nada. <code>options</code> con <code>search_path</code> <strong>sí</strong> importa: ignorado, el cliente verá el <code>search_path</code> por defecto de la base y las consultas sin esquema fallan con <code>relation does not exist</code>. La solución limpia es el rol:</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 solo enruta bases que estén en <code>[databases]</code>. O añades la entrada, o pones un comodín:</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>Con <code>*</code>, un cliente que pida <code>otra_db</code> obtiene <code>host=db.example dbname=otra_db</code>; esas entradas automáticas se limpian solas tras <code>autodb_idle_timeout</code>. Y comprueba el <code>dbname=</code> de la entrada: es habitual apuntar <code>app_db</code> a otra base por un copy-paste.</p>
<h2 id="trampas">Trampas</h2>
<ul>
<li><strong><code>RELOAD</code> basta</strong> para <code>ignore_startup_parameters</code> y <code>[databases]</code>; <code>listen_*</code> y <code>auth_type</code> piden reinicio.</li>
<li><strong>Modo <code>transaction</code> y <code>SET</code></strong>: un <code>SET search_path</code> a mano en la sesión se pierde en cuanto la conexión de servidor pasa a otro cliente; usa <code>SET LOCAL</code> dentro de la transacción o el <code>alter role</code>.</li>
<li><strong>Prepared statements</strong> con nombre (JDBC <code>prepareThreshold</code>, <code>PREPARE</code>) no funcionan en modo <code>transaction</code> en pgbouncer antiguo; desde 1.21, <code>max_prepared_statements = 200</code> los soporta.</li>
<li><strong>SCRAM</strong>: con <code>auth_type = scram-sha-256</code> la <code>userlist.txt</code> necesita el hash SCRAM tal cual sale de <code>pg_authid</code>, o <code>auth_query</code> para que pgbouncer lo consulte.</li>
<li><code>SHOW CONFIG;</code> y <code>SHOW DATABASES;</code> en la consola de admin te dicen qué está cargado de verdad, no lo que crees haber escrito.</li>
</ul>
<p>Ver también: <a href="/es/posts/databases/postgresql-cheat-sheet-administracion/">PostgreSQL: cheat sheet de administración</a>. Referencia: <a href="https://www.pgbouncer.org/config.html">pgbouncer.ini</a>.</p>
]]></content:encoded></item><item><title>PostgreSQL: cheat sheet de administración (filas por tabla, tamaños, sesiones, usuario de solo lectura)</title><link>https://inet.sh/es/posts/databases/postgresql-cheat-sheet-administracion/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://inet.sh/es/posts/databases/postgresql-cheat-sheet-administracion/</guid><description>Las consultas que un sysadmin repite en PostgreSQL: contar las filas de todas las tablas en una sola consulta (query_to_xml + xpath) o estimarlas al instante (n_live_tup), tamaños de base y tablas, quién está conectado y cómo expulsarlo, usuario de solo lectura con pg_read_all_data (14+) o con GRANT y ALTER DEFAULT PRIVILEGES, timezone por base o rol, y los flags de psql que evitan sustos.</description><content:encoded><![CDATA[<h2 id="en-corto">En corto</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">-- Filas exactas de todas las tablas de un esquema, en una sola consulta
</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">-- Estimación instantánea (no lee las tablas)
</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">-- Usuario de solo lectura (PostgreSQL 14 o superior)
</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="contar-filas">Contar filas</h2>
<p><code>count(*)</code> exacto sobre cada tabla exige recorrerla entera; en una base de cientos de GB tarda. Lo que casi siempre quieres es la <strong>estimación</strong> que mantiene el autovacuum, que se actualiza en cada <code>ANALYZE</code> y suele estar a un par de puntos del real:</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>Cuando necesitas el número exacto de <strong>todas</strong> las tablas sin escribir un <code>DO</code> en PL/pgSQL, el truco de <code>query_to_xml</code>: ejecuta un <code>count(*)</code> dinámico por tabla, devuelve el resultado como XML y <code>xpath</code> extrae el número. Es SQL plano, funciona desde cualquier cliente, y se puede filtrar por esquema o por nombre. Solo recuerda que corre los conteos en serie: en tablas grandes, paciencia.</p>
<h2 id="tamaños">Tamaños</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">-- datos + índices + 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="sesiones">Sesiones</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">-- Quién está conectado y qué hace
</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">-- Cancelar una consulta (la sesión sigue)
</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">-- Expulsar a todos menos a ti (antes de un DROP o un 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">-- O, desde PostgreSQL 13, directo:
</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="usuario-de-solo-lectura">Usuario de solo lectura</h2>
<p>En <strong>14 o superior</strong>, un rol predefinido lo resuelve para todos los esquemas, incluidas las tablas que se creen mañana:</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>En versiones anteriores, o si quieres limitarlo a un esquema, el camino clásico:</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">-- tablas futuras: solo cubre las que cree el rol que ejecuta esto (o 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">-- Deshacer
</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> no toca lo que ya existe y solo aplica a objetos creados por el rol indicado: si otra cuenta crea tablas, <code>reporting</code> no las verá. De ahí el valor de <code>pg_read_all_data</code>.</p>
<h2 id="contraseñas-timezone-y-psql">Contraseñas, timezone y 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">-- desde psql, sin dejar la contraseña en el historial
</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">-- nuevas conexiones a esa base
</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">-- nuevas sesiones de ese rol
</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"># un script que se detiene en el primer 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"># sin paginador (o \pset pager 0 dentro)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Y dentro de <code>psql</code>, <code>\x auto</code> (columnas verticales cuando no caben) y <code>\timing</code>.</p>
<h2 id="mantenimiento-mínimo">Mantenimiento mínimo</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+, sin bloquear escrituras
</span></span></span></code></pre></td></tr></table>
</div>
</div><p><code>VACUUM FULL</code> reescribe la tabla y la bloquea mientras dura; para recuperar espacio en producción, <code>pg_repack</code>.</p>
<h2 id="trampas">Trampas</h2>
<ul>
<li><strong><code>n_live_tup</code> es una estimación</strong>: recién analizada es fiel; en una tabla con mucho churn puede desviarse hasta el siguiente autoanalyze.</li>
<li><strong><code>pg_read_all_data</code> no salta RLS</strong> ni concede DDL; es lectura de tablas, vistas y secuencias, nada más.</li>
<li><strong><code>pg_terminate_backend</code> corta transacciones</strong>: la aplicación ve un error y reconecta. Para solo abortar una consulta, <code>pg_cancel_backend</code>.</li>
<li><strong><code>alter role … set timezone</code></strong> aplica a sesiones nuevas; la que ya está abierta sigue igual.</li>
<li><strong><code>quote_ident</code>/<code>format('%I')</code></strong> siempre que construyas SQL con nombres de tablas: un nombre con mayúsculas o guiones rompe la consulta sin ellos.</li>
</ul>
<h2 id="ver-también">Ver también</h2>
<ul>
<li><a href="/es/posts/databases/pgbouncer-unsupported-startup-parameter/">pgbouncer: unsupported startup parameter</a></li>
<li><a href="/es/posts/gcp/cloud-sql-importar-dumps-definer-gtid-extensiones/">Cloud SQL: importar dumps sin errores de DEFINER, GTID o extensiones</a></li>
</ul>
]]></content:encoded></item></channel></rss>