<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: bertrand HARTWIG</title>
    <description>The latest articles on DEV Community by bertrand HARTWIG (@bertrand_hartwig_309d1958).</description>
    <link>https://dev.to/bertrand_hartwig_309d1958</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1915824%2Fb0620e35-b10d-46de-9781-576785aba401.jpg</url>
      <title>DEV Community: bertrand HARTWIG</title>
      <link>https://dev.to/bertrand_hartwig_309d1958</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bertrand_hartwig_309d1958"/>
    <language>en</language>
    <item>
      <title>Running PostgreSQL Correctly with Docker Compose</title>
      <dc:creator>bertrand HARTWIG</dc:creator>
      <pubDate>Wed, 17 Jun 2026 15:01:31 +0000</pubDate>
      <link>https://dev.to/bertrand_hartwig_309d1958/running-postgresql-correctly-with-docker-compose-20i</link>
      <guid>https://dev.to/bertrand_hartwig_309d1958/running-postgresql-correctly-with-docker-compose-20i</guid>
      <description>&lt;p&gt;This guide explains how to run a PostgreSQL instance with Docker Compose using a configuration that provides a solid baseline for a properly configured PostgreSQL deployment. (excluding backup and monitoring).&lt;/p&gt;

&lt;p&gt;The goal is not only to start PostgreSQL, but to start it with sane defaults, persistent storage, proper health checks, query observability, and memory settings aligned with the resources allocated to the container.&lt;/p&gt;

&lt;p&gt;The example below is generated by &lt;strong&gt;&lt;a href="https://github.com/beh74/pgassistant-community" rel="noopener noreferrer"&gt;pgAssistant&lt;/a&gt;&lt;/strong&gt;, which provides this kind of PostgreSQL Docker configuration for free. &lt;/p&gt;

&lt;h2&gt;
  
  
  Why this configuration matters
&lt;/h2&gt;

&lt;p&gt;A PostgreSQL container should not be treated as a simple throwaway service when it stores application data. Several Docker Compose options have a direct impact on reliability, performance, security, and observability.&lt;/p&gt;

&lt;p&gt;The most important points are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;restart: always&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shm_size&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;persistent volumes&lt;/li&gt;
&lt;li&gt;container CPU and memory limits&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POSTGRES_INITDB_ARGS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;a proper health check&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pg_stat_statements&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;autovacuum=on&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;PostgreSQL tuning parameters generated from pgTune&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example &lt;code&gt;docker-compose.yml&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;northwind-db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:17-alpine&lt;/span&gt;
    &lt;span class="na"&gt;shm_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1GB&lt;/span&gt;

    &lt;span class="c1"&gt;# Optional: Use tmpfs for shared memory when running in Swarm mode&lt;/span&gt;
    &lt;span class="c1"&gt;# volumes:&lt;/span&gt;
    &lt;span class="c1"&gt;#   - type: tmpfs&lt;/span&gt;
    &lt;span class="c1"&gt;#     target: /dev/shm&lt;/span&gt;
    &lt;span class="c1"&gt;#     tmpfs:&lt;/span&gt;
    &lt;span class="c1"&gt;#       size: 1073741824&lt;/span&gt;

    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;northwind_data:/var/lib/postgresql/data&lt;/span&gt;

    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432:5432"&lt;/span&gt;

    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6.0"&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4GB&lt;/span&gt;

    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_USER=postgres&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_PASSWORD=xxxxx&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_DB=northwind&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;POSTGRES_INITDB_ARGS=--auth-local=scram-sha-256 --auth-host=scram-sha-256&lt;/span&gt;

    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-U"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgres"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;postgres&lt;/span&gt;
        &lt;span class="s"&gt;-c shared_preload_libraries='pg_stat_statements'&lt;/span&gt;
        &lt;span class="s"&gt;-c autovacuum=on&lt;/span&gt;
        &lt;span class="s"&gt;-c max_connections=200&lt;/span&gt;
        &lt;span class="s"&gt;-c shared_buffers='1GB'&lt;/span&gt;
        &lt;span class="s"&gt;-c effective_cache_size='3GB'&lt;/span&gt;
        &lt;span class="s"&gt;-c maintenance_work_mem='256MB'&lt;/span&gt;
        &lt;span class="s"&gt;-c checkpoint_completion_target=0.9&lt;/span&gt;
        &lt;span class="s"&gt;-c wal_buffers='16MB'&lt;/span&gt;
        &lt;span class="s"&gt;-c default_statistics_target=100&lt;/span&gt;
        &lt;span class="s"&gt;-c random_page_cost=1.1&lt;/span&gt;
        &lt;span class="s"&gt;-c effective_io_concurrency=200&lt;/span&gt;
        &lt;span class="s"&gt;-c work_mem='5242kB'&lt;/span&gt;
        &lt;span class="s"&gt;-c min_wal_size='1GB'&lt;/span&gt;
        &lt;span class="s"&gt;-c max_wal_size='4GB'&lt;/span&gt;
        &lt;span class="s"&gt;-c huge_pages='off'&lt;/span&gt;
        &lt;span class="s"&gt;-c max_worker_processes=6&lt;/span&gt;
        &lt;span class="s"&gt;-c max_parallel_workers=6&lt;/span&gt;
        &lt;span class="s"&gt;-c max_parallel_maintenance_workers=3&lt;/span&gt;
        &lt;span class="s"&gt;-c max_parallel_workers_per_gather=3&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;northwind_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  restart: always
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This option tells Docker to automatically restart the PostgreSQL container if it stops unexpectedly.&lt;/p&gt;

&lt;p&gt;For a database service, this is important because PostgreSQL is usually a critical dependency for the application. Without a restart policy, the database may remain stopped after a crash, host reboot, or Docker daemon restart.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;restart: always&lt;/code&gt; does not replace monitoring, backups, replication, or high availability, but it provides a basic resilience layer that should almost always be enabled for a database container.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shared memory and &lt;code&gt;shm_size&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;shm_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1GB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL relies on shared memory for several internal operations. Docker containers have a default shared memory size that is often too small for a properly tuned PostgreSQL instance.&lt;/p&gt;

&lt;p&gt;A good rule is to align &lt;code&gt;shm_size&lt;/code&gt; with the PostgreSQL &lt;code&gt;shared_buffers&lt;/code&gt; value.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;shm_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512MB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;should be consistent with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;-c shared_buffers='512MB'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the example generated by pgAssistant, both values are set to &lt;code&gt;1GB&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;shm_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1GB&lt;/span&gt;
&lt;span class="s"&gt;-c shared_buffers='1GB'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is important because &lt;code&gt;shared_buffers&lt;/code&gt; defines how much memory PostgreSQL uses for its own shared buffer cache. If Docker shared memory is undersized compared to PostgreSQL configuration, PostgreSQL may fail to start or behave poorly under load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persistent volume
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;northwind_data:/var/lib/postgresql/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;mount point is diffrent with postgresql 18 :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;northwind_data:/var/lib/postgresql&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A PostgreSQL database must use persistent storage.&lt;/p&gt;

&lt;p&gt;The directory &lt;code&gt;/var/lib/postgresql/data&lt;/code&gt; or &lt;code&gt;/var/lib/postgresql&lt;/code&gt; with v18 is where the official PostgreSQL Docker image stores the database cluster. If this directory is not backed by a Docker volume or another persistent storage mechanism, the data may be lost when the container is removed.&lt;/p&gt;

&lt;p&gt;The named volume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;northwind_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ensures that database files survive container recreation.&lt;/p&gt;

&lt;p&gt;This is one of the most important parts of the configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Container resources
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6.0"&lt;/span&gt;
      &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4GB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CPU and memory allocated to the container are not just Docker-level constraints. They are also the values that should be used to calculate PostgreSQL tuning parameters.&lt;/p&gt;

&lt;p&gt;Tools such as pgTune need to know how many CPUs and how much memory are available to PostgreSQL. The values used in pgTune must match the resources allocated to the container.&lt;/p&gt;

&lt;p&gt;For example, if the container is limited to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6.0"&lt;/span&gt;
&lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4GB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then pgTune should be configured using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;6 CPUs&lt;/li&gt;
&lt;li&gt;4 GB of RAM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using host-level resources instead of container-level resources would produce an incorrect PostgreSQL configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  POSTGRES_INITDB_ARGS
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;POSTGRES_INITDB_ARGS=--auth-local=scram-sha-256 --auth-host=scram-sha-256&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;POSTGRES_INITDB_ARGS&lt;/code&gt; allows passing arguments to &lt;code&gt;initdb&lt;/code&gt; when the PostgreSQL data directory is initialized for the first time.&lt;/p&gt;

&lt;p&gt;In this example, it enables SCRAM-SHA-256 authentication for both local and host connections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--auth-local&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;scram-sha-256
&lt;span class="nt"&gt;--auth-host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;scram-sha-256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is important because authentication settings are part of the initial database cluster setup.&lt;/p&gt;

&lt;p&gt;Note that these options only apply when the PostgreSQL data directory is empty. If the persistent volume already contains an initialized database, changing &lt;code&gt;POSTGRES_INITDB_ARGS&lt;/code&gt; will not reinitialize the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Health check
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$$POSTGRES_USER&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$$POSTGRES_DB"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
  &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
  &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A health check allows Docker to determine whether PostgreSQL is actually ready to accept connections.&lt;/p&gt;

&lt;p&gt;This is different from simply checking whether the container process is running. PostgreSQL may be running but not ready yet, especially during startup, crash recovery, or initialization.&lt;/p&gt;

&lt;p&gt;The command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pg_isready &lt;span class="nt"&gt;-U&lt;/span&gt; postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;checks the readiness of the PostgreSQL server.&lt;/p&gt;

&lt;p&gt;A proper health check is important when other services depend on the database. It allows orchestration tools, deployment scripts, and dependent containers to wait until PostgreSQL is ready before connecting.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;pg_stat_statements&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;-c shared_preload_libraries='pg_stat_statements'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pg_stat_statements&lt;/code&gt; is essential for PostgreSQL query optimization.&lt;/p&gt;

&lt;p&gt;It tracks SQL execution statistics, including how often queries run, how long they take, and how much load they generate. This makes it possible to identify expensive queries that need indexes, rewriting, caching, or schema improvements.&lt;/p&gt;

&lt;p&gt;Enabling it requires two steps.&lt;/p&gt;

&lt;p&gt;First, it must be loaded at server startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;-c shared_preload_libraries='pg_stat_statements'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, the extension must be created inside the target database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pg_stat_statements&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A common way to do this with Docker is to add an initialization SQL script under &lt;code&gt;/docker-entrypoint-initdb.d/&lt;/code&gt;, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;pg_stat_statements&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;pg_stat_statements&lt;/code&gt;, database optimization is mostly guesswork. With it, you can focus on the queries that actually consume time and resources.&lt;/p&gt;

&lt;p&gt;Useful example query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;total_exec_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;mean_exec_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;rows&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_statements&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_exec_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;code&gt;autovacuum=on&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;-c autovacuum=on&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Autovacuum should remain enabled.&lt;/p&gt;

&lt;p&gt;PostgreSQL uses MVCC, which means updates and deletes leave behind dead tuples. Autovacuum cleans up these dead tuples and helps prevent table and index bloat.&lt;/p&gt;

&lt;p&gt;Disabling autovacuum is dangerous for most applications. It can lead to degraded performance, excessive storage growth, transaction ID wraparound risks, and poor query plans due to outdated statistics.&lt;/p&gt;

&lt;p&gt;In most cases, the right approach is not to disable autovacuum, but to tune it if the workload requires it.&lt;/p&gt;

&lt;h2&gt;
  
  
  pgTune-generated PostgreSQL parameters
&lt;/h2&gt;

&lt;p&gt;The remaining PostgreSQL parameters passed to the &lt;code&gt;postgres&lt;/code&gt; command are generated from pgTune:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;-c max_connections=200&lt;/span&gt;
&lt;span class="s"&gt;-c shared_buffers='1GB'&lt;/span&gt;
&lt;span class="s"&gt;-c effective_cache_size='3GB'&lt;/span&gt;
&lt;span class="s"&gt;-c maintenance_work_mem='256MB'&lt;/span&gt;
&lt;span class="s"&gt;-c checkpoint_completion_target=0.9&lt;/span&gt;
&lt;span class="s"&gt;-c wal_buffers='16MB'&lt;/span&gt;
&lt;span class="s"&gt;-c default_statistics_target=100&lt;/span&gt;
&lt;span class="s"&gt;-c random_page_cost=1.1&lt;/span&gt;
&lt;span class="s"&gt;-c effective_io_concurrency=200&lt;/span&gt;
&lt;span class="s"&gt;-c work_mem='5242kB'&lt;/span&gt;
&lt;span class="s"&gt;-c min_wal_size='1GB'&lt;/span&gt;
&lt;span class="s"&gt;-c max_wal_size='4GB'&lt;/span&gt;
&lt;span class="s"&gt;-c huge_pages='off'&lt;/span&gt;
&lt;span class="s"&gt;-c max_worker_processes=6&lt;/span&gt;
&lt;span class="s"&gt;-c max_parallel_workers=6&lt;/span&gt;
&lt;span class="s"&gt;-c max_parallel_maintenance_workers=3&lt;/span&gt;
&lt;span class="s"&gt;-c max_parallel_workers_per_gather=3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These values should not be copied blindly from another machine.&lt;/p&gt;

&lt;p&gt;They depend on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;allocated memory&lt;/li&gt;
&lt;li&gt;available CPUs&lt;/li&gt;
&lt;li&gt;storage type&lt;/li&gt;
&lt;li&gt;expected workload&lt;/li&gt;
&lt;li&gt;maximum number of connections&lt;/li&gt;
&lt;li&gt;PostgreSQL version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;pgTune provides a strong baseline configuration based on the resources allocated to the PostgreSQL container. It does not replace workload-specific tuning, but it gives a much better starting point than PostgreSQL defaults for many real-world deployments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Important relationship between Docker resources and pgTune
&lt;/h2&gt;

&lt;p&gt;The values used in pgTune must match the resources actually available to PostgreSQL.&lt;/p&gt;

&lt;p&gt;For example, this Docker resource limit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6.0"&lt;/span&gt;
&lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4GB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;must be reflected in the pgTune input.&lt;/p&gt;

&lt;p&gt;That is why pgAssistant generates both:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the Docker resource limits, and&lt;/li&gt;
&lt;li&gt;the PostgreSQL runtime parameters derived from those limits.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This avoids a common mistake: tuning PostgreSQL for the host machine while the container is limited to fewer resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  About pgAssistant
&lt;/h2&gt;

&lt;p&gt;pgAssistant generates this kind of PostgreSQL Docker Compose configuration for free.&lt;/p&gt;

&lt;p&gt;It helps create a more reliable and better-tuned PostgreSQL setup&lt;/p&gt;

&lt;p&gt;This provides a solid minimum configuration for running PostgreSQL properly with Docker.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>postgressql</category>
      <category>docker</category>
    </item>
    <item>
      <title>What’s New in pgAssistant Since Version 2.8</title>
      <dc:creator>bertrand HARTWIG</dc:creator>
      <pubDate>Sun, 07 Jun 2026 05:48:09 +0000</pubDate>
      <link>https://dev.to/bertrand_hartwig_309d1958/whats-new-in-pgassistant-since-version-28-2l7l</link>
      <guid>https://dev.to/bertrand_hartwig_309d1958/whats-new-in-pgassistant-since-version-28-2l7l</guid>
      <description>&lt;h1&gt;
  
  
  What’s New in pgAssistant Since Version 2.8
&lt;/h1&gt;

&lt;p&gt;Since version 2.8, pgAssistant has evolved significantly.&lt;/p&gt;

&lt;p&gt;The initial goal was to introduce a &lt;strong&gt;Global Advisor&lt;/strong&gt; capable of combining multiple PostgreSQL signals—schema design, indexes, maintenance statistics, configuration, and workload activity—to provide higher-level recommendations.&lt;/p&gt;

&lt;p&gt;Several releases later, this experimental feature has become a much more mature expert system. At the same time, the Query Advisor, Index Advisor, ranking engine, collector integrations, and developer-facing maintenance views have also improved.&lt;/p&gt;

&lt;p&gt;This article summarizes the main changes introduced between &lt;strong&gt;pgAssistant 2.8.0 and 2.9.2&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Global Advisor: from an experiment to a broader expert system
&lt;/h2&gt;

&lt;p&gt;Version 2.8 introduced the first version of the Global Advisor.&lt;/p&gt;

&lt;p&gt;Its purpose was to move beyond isolated query analysis and evaluate the database as a whole. Instead of looking only at a single execution plan, pgAssistant started correlating signals from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;database schemas;&lt;/li&gt;
&lt;li&gt;foreign keys;&lt;/li&gt;
&lt;li&gt;indexes;&lt;/li&gt;
&lt;li&gt;table statistics;&lt;/li&gt;
&lt;li&gt;vacuum and analyze activity;&lt;/li&gt;
&lt;li&gt;configuration settings;&lt;/li&gt;
&lt;li&gt;storage usage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first release was intentionally experimental, but the advisor quickly expanded.&lt;/p&gt;

&lt;p&gt;By version 2.8.2, the Global Advisor included &lt;strong&gt;14 different recommendations&lt;/strong&gt;, together with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a summary of the detected findings;&lt;/li&gt;
&lt;li&gt;clearer recommendation grouping;&lt;/li&gt;
&lt;li&gt;a button to display all generated SQL suggestions;&lt;/li&gt;
&lt;li&gt;one-click copying of all suggested commands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This made the advisor more useful as both a diagnostic tool and a review checklist.&lt;/p&gt;




&lt;h2&gt;
  
  
  Better foreign-key diagnostics
&lt;/h2&gt;

&lt;p&gt;Foreign keys were one of the first areas improved after the initial Global Advisor release.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data type mismatches
&lt;/h3&gt;

&lt;p&gt;pgAssistant now detects foreign-key columns whose data types differ from the referenced columns.&lt;/p&gt;

&lt;p&gt;The detection logic was refined to reduce false positives and provide clearer remediation guidance.&lt;/p&gt;

&lt;p&gt;The recommendation also takes into account that changing a column type can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rewrite the table;&lt;/li&gt;
&lt;li&gt;rebuild dependent indexes;&lt;/li&gt;
&lt;li&gt;acquire an &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; lock;&lt;/li&gt;
&lt;li&gt;require additional disk space;&lt;/li&gt;
&lt;li&gt;require a maintenance window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The suggested SQL includes both the type change and a subsequent &lt;code&gt;ANALYZE&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Missing foreign-key indexes
&lt;/h3&gt;

&lt;p&gt;The advisor also detects useful indexes missing from foreign-key columns.&lt;/p&gt;

&lt;p&gt;The logic now considers the size of both the child and referenced tables, reducing noise for very small tables and prioritizing cases where the index is more likely to improve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;joins;&lt;/li&gt;
&lt;li&gt;lookups;&lt;/li&gt;
&lt;li&gt;parent-table &lt;code&gt;UPDATE&lt;/code&gt; operations;&lt;/li&gt;
&lt;li&gt;parent-table &lt;code&gt;DELETE&lt;/code&gt; operations.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Stronger index diagnostics
&lt;/h2&gt;

&lt;p&gt;Index analysis has received several important improvements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better index recommendations from execution plans
&lt;/h3&gt;

&lt;p&gt;The Index Advisor can now identify a potentially better index even when an execution-plan node already uses an existing index.&lt;/p&gt;

&lt;p&gt;Previously, the presence of an index scan could hide opportunities for a more selective or better-aligned index. The new logic evaluates whether the existing index is actually the best available access path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Duplicate and redundant indexes
&lt;/h3&gt;

&lt;p&gt;The Global Advisor now identifies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;strictly duplicate indexes;&lt;/li&gt;
&lt;li&gt;unused indexes;&lt;/li&gt;
&lt;li&gt;non-unique indexes fully covered by equivalent unique indexes;&lt;/li&gt;
&lt;li&gt;tables with an unusually high index-to-table size ratio.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For strictly duplicate indexes, pgAssistant now keeps the &lt;strong&gt;most-used equivalent index&lt;/strong&gt; rather than selecting one only by object identifier.&lt;/p&gt;

&lt;p&gt;This is important because identical index definitions do not necessarily have identical operational value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safer unused-index detection
&lt;/h3&gt;

&lt;p&gt;An index with &lt;code&gt;idx_scan = 0&lt;/code&gt; is not automatically useless.&lt;/p&gt;

&lt;p&gt;The counter may have been reset recently, or the observed workload may not yet be representative.&lt;/p&gt;

&lt;p&gt;The unused-index checks now expose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the database statistics reset timestamp;&lt;/li&gt;
&lt;li&gt;the observation age;&lt;/li&gt;
&lt;li&gt;table scan activity;&lt;/li&gt;
&lt;li&gt;table write activity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A generic unused index is reported only when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;at least 24 hours of statistics are available; and&lt;/li&gt;
&lt;li&gt;the table has experienced meaningful activity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The current thresholds require at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100 table scans; or&lt;/li&gt;
&lt;li&gt;1,000 row modifications.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For structurally redundant indexes, the statistics age remains informational rather than blocking the recommendation, because the redundancy can already be proven from PostgreSQL system catalogs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Invalid index handling
&lt;/h3&gt;

&lt;p&gt;The invalid-index recommendation has also been redesigned.&lt;/p&gt;

&lt;p&gt;Instead of always suggesting a drop and manual recreation, pgAssistant now prefers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;REINDEX&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This preserves the index definition and is generally safer for indexes supporting constraints.&lt;/p&gt;

&lt;p&gt;The advisor also recognizes common artifacts left by failed concurrent rebuilds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;_ccnew&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;_ccold&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These cases receive specific guidance, because blindly rebuilding or dropping such indexes could leave duplicates or remove the wrong object.&lt;/p&gt;




&lt;h2&gt;
  
  
  PostgreSQL release and support checks
&lt;/h2&gt;

&lt;p&gt;Version 2.9.1 introduced PostgreSQL release checks in the Global Advisor.&lt;/p&gt;

&lt;p&gt;pgAssistant retrieves the official release information from:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.postgresql.org/versions.json" rel="noopener noreferrer"&gt;https://www.postgresql.org/versions.json&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It can now detect two different situations.&lt;/p&gt;

&lt;h3&gt;
  
  
  A newer minor release is available
&lt;/h3&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PostgreSQL 17.6 is not the latest minor release available for branch 17. Upgrade to PostgreSQL 17.10, released on 2026-05-14, to benefit from the latest bug, security, reliability, and data-integrity fixes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The recommendation includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the installed release;&lt;/li&gt;
&lt;li&gt;the latest release in the same major branch;&lt;/li&gt;
&lt;li&gt;the publication date of that release;&lt;/li&gt;
&lt;li&gt;a recommendation to review intermediate release notes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The PostgreSQL branch is no longer supported
&lt;/h3&gt;

&lt;p&gt;The advisor also reports end-of-life versions, even if the final minor release is already installed.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PostgreSQL 13.23 is the latest minor release of the 13 branch, but this branch is no longer supported. Plan a major upgrade to a supported PostgreSQL version. End-of-life date: 2025-11-13.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This distinction matters because installing the latest minor release does not restore support for an obsolete major branch.&lt;/p&gt;




&lt;h2&gt;
  
  
  More accurate maintenance recommendations
&lt;/h2&gt;

&lt;p&gt;Maintenance recommendations were refined to reduce unnecessary findings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tables never vacuumed
&lt;/h3&gt;

&lt;p&gt;The previous implementation could report very small tables simply because no vacuum timestamp was available.&lt;/p&gt;

&lt;p&gt;The rule now requires stronger evidence, combining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;table size;&lt;/li&gt;
&lt;li&gt;an absolute number of dead tuples;&lt;/li&gt;
&lt;li&gt;dead tuple percentage;&lt;/li&gt;
&lt;li&gt;absence of recorded vacuum activity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This prevents pgAssistant from recommending maintenance for insignificant tables containing only a few rows or dead tuples.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stale statistics
&lt;/h3&gt;

&lt;p&gt;The stale-statistics check considers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the age of the last manual or automatic analyze;&lt;/li&gt;
&lt;li&gt;the amount of data modified since the last analyze;&lt;/li&gt;
&lt;li&gt;the table size;&lt;/li&gt;
&lt;li&gt;the modification ratio.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The recommendation is therefore based on workload and table significance rather than only on a timestamp.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vacuum recommendations
&lt;/h3&gt;

&lt;p&gt;Starting with version 2.8.4, pgAssistant recommends &lt;code&gt;ANALYZE&lt;/code&gt; or &lt;code&gt;VACUUM&lt;/code&gt; only when the latest relevant maintenance operation is older than six days.&lt;/p&gt;

&lt;p&gt;This reduces repetitive advice for tables that were maintained recently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Autovacuum urgency
&lt;/h3&gt;

&lt;p&gt;A dedicated recommendation calculates whether a table has exceeded its effective autovacuum threshold.&lt;/p&gt;

&lt;p&gt;The calculation considers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;global autovacuum settings;&lt;/li&gt;
&lt;li&gt;table-specific reloptions;&lt;/li&gt;
&lt;li&gt;estimated row count;&lt;/li&gt;
&lt;li&gt;dead tuple count;&lt;/li&gt;
&lt;li&gt;configured maximum thresholds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives a more meaningful signal than a simple dead tuple percentage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Version-aware PostgreSQL configuration checks
&lt;/h2&gt;

&lt;p&gt;Configuration recommendations now consider the installed PostgreSQL version.&lt;/p&gt;

&lt;p&gt;This is necessary because default values have changed between major releases.&lt;/p&gt;

&lt;p&gt;The advisor checks settings such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;autovacuum&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;track_counts&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;track_activities&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;log_checkpoints&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;log_autovacuum_min_duration&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;checkpoint_completion_target&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;checkpoint_timeout&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, &lt;code&gt;checkpoint_completion_target&lt;/code&gt; used a lower default in older PostgreSQL releases, while logging defaults also changed in more recent versions.&lt;/p&gt;

&lt;p&gt;The recommendation text now explains whether a value was historically normal for that PostgreSQL version or differs from a modern default.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sequence exhaustion detection
&lt;/h2&gt;

&lt;p&gt;Version 2.8.3 introduced a recommendation for sequences approaching their maximum value.&lt;/p&gt;

&lt;p&gt;pgAssistant calculates the percentage of the available sequence range already consumed and reports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a medium-level warning above 75%;&lt;/li&gt;
&lt;li&gt;a high-level warning above 90%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This helps identify future insert failures before the sequence is exhausted.&lt;/p&gt;




&lt;h2&gt;
  
  
  Better workload prioritization
&lt;/h2&gt;

&lt;p&gt;The query ranking algorithm was improved in version 2.8.4.&lt;/p&gt;

&lt;p&gt;Sorting queries only by mean execution time or total execution time often produces misleading priorities.&lt;/p&gt;

&lt;p&gt;A query executed once may be slow but have little overall impact, while a moderately slow query executed thousands of times can consume much more of the workload.&lt;/p&gt;

&lt;p&gt;The updated ranking gives more weight to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;execution frequency;&lt;/li&gt;
&lt;li&gt;total workload impact;&lt;/li&gt;
&lt;li&gt;repeatability;&lt;/li&gt;
&lt;li&gt;technical signals.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the same time, it reduces the influence of low-impact one-off queries.&lt;/p&gt;

&lt;p&gt;The result is a more practical answer to the question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which PostgreSQL queries should I optimize first?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Richer context for AI-assisted query analysis
&lt;/h2&gt;

&lt;p&gt;The query-analysis prompt is now enriched with column statistics collected by pgAssistant.&lt;/p&gt;

&lt;p&gt;Execution plans alone rarely provide enough context for accurate recommendations.&lt;/p&gt;

&lt;p&gt;The additional statistics help the AI reason about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cardinality;&lt;/li&gt;
&lt;li&gt;null fractions;&lt;/li&gt;
&lt;li&gt;common values;&lt;/li&gt;
&lt;li&gt;data distributions;&lt;/li&gt;
&lt;li&gt;selectivity;&lt;/li&gt;
&lt;li&gt;correlations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This reinforces pgAssistant’s hybrid approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deterministic expert-system rules for reliable technical diagnostics;&lt;/li&gt;
&lt;li&gt;AI for explanation, synthesis, and contextual analysis.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Table Definition Helper redesign
&lt;/h2&gt;

&lt;p&gt;The Table Definition Helper interface was redesigned to provide a clearer view of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;table size;&lt;/li&gt;
&lt;li&gt;index footprint;&lt;/li&gt;
&lt;li&gt;row estimates;&lt;/li&gt;
&lt;li&gt;dead tuples;&lt;/li&gt;
&lt;li&gt;estimated bloat;&lt;/li&gt;
&lt;li&gt;schema information.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The new card-based interface includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;summary indicators;&lt;/li&gt;
&lt;li&gt;immediate search while typing;&lt;/li&gt;
&lt;li&gt;client-side filters;&lt;/li&gt;
&lt;li&gt;sorting;&lt;/li&gt;
&lt;li&gt;pagination;&lt;/li&gt;
&lt;li&gt;visual severity levels.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes it easier to navigate large databases without repeatedly submitting server-side searches.&lt;/p&gt;




&lt;h2&gt;
  
  
  New Table Health view
&lt;/h2&gt;

&lt;p&gt;Version 2.9.2 introduces a new &lt;strong&gt;Table Health&lt;/strong&gt; page in the DBA Corner.&lt;/p&gt;

&lt;p&gt;Despite its location, the goal is not to turn pgAssistant into a general-purpose DBA console.&lt;/p&gt;

&lt;p&gt;The purpose remains developer-oriented:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Help developers understand the state of a table before escalating the issue to a DBA.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For every schema and table, the view displays:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;table size;&lt;/li&gt;
&lt;li&gt;index size;&lt;/li&gt;
&lt;li&gt;estimated row count;&lt;/li&gt;
&lt;li&gt;dead tuple count and percentage;&lt;/li&gt;
&lt;li&gt;tuples modified since the last analyze;&lt;/li&gt;
&lt;li&gt;update activity;&lt;/li&gt;
&lt;li&gt;HOT update percentage;&lt;/li&gt;
&lt;li&gt;latest manual vacuum;&lt;/li&gt;
&lt;li&gt;latest autovacuum;&lt;/li&gt;
&lt;li&gt;latest manual analyze;&lt;/li&gt;
&lt;li&gt;latest autoanalyze;&lt;/li&gt;
&lt;li&gt;vacuum age;&lt;/li&gt;
&lt;li&gt;analyze age;&lt;/li&gt;
&lt;li&gt;database statistics reset timestamp.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tables are classified using statuses such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;HEALTHY&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ANALYZE_DUE&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NEVER_ANALYZED&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HIGH_DEAD_TUPLES&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Users can filter, search, sort, and inspect tables immediately.&lt;/p&gt;

&lt;p&gt;They can also launch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="k"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;VACUUM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;through the existing SQL execution endpoint, with a confirmation dialog showing the exact command and execution result.&lt;/p&gt;




&lt;h2&gt;
  
  
  Collector and Grafana integration
&lt;/h2&gt;

&lt;p&gt;Version 2.9.0 added two API endpoints used by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/beh74/pgassistant-collector" rel="noopener noreferrer"&gt;pgAssistant Collector&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/beh74/pgassistant-grafana" rel="noopener noreferrer"&gt;pgAssistant Grafana&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows pgAssistant findings to be collected over time and visualized across multiple PostgreSQL instances.&lt;/p&gt;

&lt;p&gt;The integration makes it possible to build fleet-level views such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recommendation evolution;&lt;/li&gt;
&lt;li&gt;database design issues;&lt;/li&gt;
&lt;li&gt;maintenance risks;&lt;/li&gt;
&lt;li&gt;corrected findings;&lt;/li&gt;
&lt;li&gt;environment-level comparisons.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Broader schema coverage
&lt;/h2&gt;

&lt;p&gt;Several Global Advisor rules were unintentionally limited to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nspname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This meant that databases using application-specific schemas could be only partially analyzed.&lt;/p&gt;

&lt;p&gt;The rules now inspect all user schemas while excluding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pg_catalog&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;information_schema&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pg_toast&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;temporary schemas;&lt;/li&gt;
&lt;li&gt;temporary TOAST schemas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This bug fix affects several important checks, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;foreign-key type mismatches;&lt;/li&gt;
&lt;li&gt;missing foreign-key indexes;&lt;/li&gt;
&lt;li&gt;duplicate indexes;&lt;/li&gt;
&lt;li&gt;redundant indexes;&lt;/li&gt;
&lt;li&gt;unused indexes.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Additional fixes and compatibility improvements
&lt;/h2&gt;

&lt;p&gt;Other changes since version 2.8 include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;support for the Qwen 3.6 model;&lt;/li&gt;
&lt;li&gt;improved connection-form behavior when using a PostgreSQL connection URI;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These fixes improve both model compatibility and the initial connection experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  A clearer direction for pgAssistant
&lt;/h2&gt;

&lt;p&gt;The releases since version 2.8 reflect a broader direction for the project.&lt;/p&gt;

&lt;p&gt;pgAssistant is not intended to replace PostgreSQL DBAs.&lt;/p&gt;

&lt;p&gt;Instead, it aims to help developers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;understand PostgreSQL behavior;&lt;/li&gt;
&lt;li&gt;identify the most important problems;&lt;/li&gt;
&lt;li&gt;distinguish strong evidence from weak signals;&lt;/li&gt;
&lt;li&gt;generate safer remediation commands;&lt;/li&gt;
&lt;li&gt;collect the context required before involving a DBA.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The product increasingly combines two complementary approaches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A deterministic expert system&lt;/strong&gt;, based on PostgreSQL catalogs, statistics, configuration, and execution plans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-assisted explanations&lt;/strong&gt;, used to summarize findings and make complex PostgreSQL behavior easier to understand.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The goal is not to automate every database decision.&lt;/p&gt;

&lt;p&gt;The goal is to make PostgreSQL diagnostics more transparent, more explainable, and more actionable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/beh74/pgassistant-community" rel="noopener noreferrer"&gt;pgAssistant Community on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://beh74.github.io/pgassistant-blog/" rel="noopener noreferrer"&gt;pgAssistant documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/beh74/pgassistant-collector" rel="noopener noreferrer"&gt;pgAssistant Collector&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/beh74/pgassistant-grafana" rel="noopener noreferrer"&gt;pgAssistant Grafana&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>PostgreSQL: Which Queries Should You Optimize First?</title>
      <dc:creator>bertrand HARTWIG</dc:creator>
      <pubDate>Thu, 14 May 2026 08:08:52 +0000</pubDate>
      <link>https://dev.to/bertrand_hartwig_309d1958/postgresql-which-queries-should-you-optimize-first-5ga</link>
      <guid>https://dev.to/bertrand_hartwig_309d1958/postgresql-which-queries-should-you-optimize-first-5ga</guid>
      <description>&lt;p&gt;When investigating PostgreSQL performance, the usual starting point is &lt;code&gt;pg_stat_statements&lt;/code&gt;. From there, many teams sort queries by &lt;code&gt;mean_exec_time&lt;/code&gt; or &lt;code&gt;total_exec_time&lt;/code&gt; and start optimizing the first rows in the list.&lt;/p&gt;

&lt;p&gt;That approach is simple, but it often leads to the wrong priorities.&lt;/p&gt;

&lt;p&gt;A query that takes five seconds but runs twice a day is not necessarily more important than a query that takes five milliseconds and runs millions of times. Conversely, a query with a high total execution time may simply be a normal core workload query, not necessarily the best optimization target.&lt;/p&gt;

&lt;p&gt;The real question is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which query is the slowest?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which query has the highest operational impact and the clearest optimization potential?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the principle behind the query-ranking algorithm implemented in &lt;strong&gt;pgAssistant&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://beh74.github.io/pgassistant-blog/post/ranking/" rel="noopener noreferrer"&gt;Read the full post here&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>postgressql</category>
      <category>performance</category>
    </item>
    <item>
      <title>Indexing every WHERE column is not PostgreSQL optimization</title>
      <dc:creator>bertrand HARTWIG</dc:creator>
      <pubDate>Mon, 11 May 2026 15:20:15 +0000</pubDate>
      <link>https://dev.to/bertrand_hartwig_309d1958/indexing-every-where-column-is-not-postgresql-optimization-3gj0</link>
      <guid>https://dev.to/bertrand_hartwig_309d1958/indexing-every-where-column-is-not-postgresql-optimization-3gj0</guid>
      <description>&lt;p&gt;One PostgreSQL indexing mistake I see often:&lt;/p&gt;

&lt;p&gt;“The query filters on A, B and C, so let’s create an index on A, B, C.”&lt;/p&gt;

&lt;p&gt;That may work, but it may also be the wrong index.&lt;/p&gt;

&lt;p&gt;For composite B-tree indexes, PostgreSQL cares about predicate type, column order, selectivity, table size, and the actual execution plan.&lt;/p&gt;

&lt;p&gt;In this post, I explain why equality predicates usually belong before range predicates, why n_distinct from statistics matters, and why a theoretically good index is useless if the planner never uses it.&lt;/p&gt;

&lt;p&gt;I also show how &lt;strong&gt;pgAssistant&lt;/strong&gt; turns this into an automated index recommendation workflow using EXPLAIN ANALYZE and planner statistics.&lt;/p&gt;

&lt;p&gt;Full write-up:&lt;br&gt;
&lt;a href="https://beh74.github.io/pgassistant-blog/post/query_advisor/" rel="noopener noreferrer"&gt;https://beh74.github.io/pgassistant-blog/post/query_advisor/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>performance</category>
    </item>
    <item>
      <title>Designing the Right PostgreSQL Index Using Query Plans and Statistics</title>
      <dc:creator>bertrand HARTWIG</dc:creator>
      <pubDate>Mon, 11 May 2026 06:04:15 +0000</pubDate>
      <link>https://dev.to/bertrand_hartwig_309d1958/designing-the-right-postgresql-index-using-query-plans-and-statistics-26d1</link>
      <guid>https://dev.to/bertrand_hartwig_309d1958/designing-the-right-postgresql-index-using-query-plans-and-statistics-26d1</guid>
      <description>&lt;p&gt;PostgreSQL index design is often misunderstood.&lt;/p&gt;

&lt;p&gt;Many developers think that creating a good index simply means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Create an index containing the columns from the WHERE clause.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In reality, efficient index design is far more nuanced.&lt;/p&gt;

&lt;p&gt;The order of columns inside a composite index matters enormously, and the best choice depends on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Predicate types (&lt;code&gt;=&lt;/code&gt;, &lt;code&gt;&amp;gt;=&lt;/code&gt;, &lt;code&gt;BETWEEN&lt;/code&gt;, &lt;code&gt;LIKE&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Column selectivity&lt;/li&gt;
&lt;li&gt;Table size&lt;/li&gt;
&lt;li&gt;PostgreSQL planner statistics&lt;/li&gt;
&lt;li&gt;Actual execution plans&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article explains the core principles behind efficient PostgreSQL index design before showing how pgAssistant automates this process using execution plans and database statistics.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Index Design Is Difficult
&lt;/h1&gt;

&lt;p&gt;Consider the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;employee_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ship_country&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;employee_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, several index definitions may appear reasonable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;employee_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;employee_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;employee_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But these indexes do &lt;strong&gt;not&lt;/strong&gt; behave the same way.&lt;/p&gt;

&lt;p&gt;Choosing the correct ordering requires understanding how PostgreSQL traverses B-Tree indexes.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Fundamental Rule of B-Tree Indexes
&lt;/h1&gt;

&lt;p&gt;For PostgreSQL B-Tree indexes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Equality predicates should come first&lt;/li&gt;
&lt;li&gt;Range predicates should come last&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the single most important rule in multi-column index design.&lt;/p&gt;




&lt;h1&gt;
  
  
  Equality Predicates Are Highly Selective
&lt;/h1&gt;

&lt;p&gt;Predicates such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;allow PostgreSQL to navigate directly to a very precise section of the index tree.&lt;/p&gt;

&lt;p&gt;In our query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;employee_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;are equality predicates.&lt;/p&gt;

&lt;p&gt;If the index begins with these columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;employee_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL can rapidly narrow the search space.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer_id = exact branch
employee_id = exact sub-branch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The planner can jump almost directly to the matching rows.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Range Predicates Should Come Last
&lt;/h1&gt;

&lt;p&gt;Now consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a range predicate.&lt;/p&gt;

&lt;p&gt;Once PostgreSQL enters a range scan inside a B-Tree index, the remaining columns become far less useful for navigation.&lt;/p&gt;

&lt;p&gt;For example, with this index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;employee_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the planner must first scan all matching dates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;order_date &amp;gt;= DATE $3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which may represent a very large portion of the table.&lt;/p&gt;

&lt;p&gt;Only afterward can additional filtering occur.&lt;/p&gt;

&lt;p&gt;This usually produces significantly more index scanning.&lt;/p&gt;

&lt;p&gt;That is why range predicates are generally placed at the end of composite indexes.&lt;/p&gt;




&lt;h1&gt;
  
  
  Column Order Among Equality Predicates
&lt;/h1&gt;

&lt;p&gt;Once equality predicates are identified, the next challenge is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which equality column should come first?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer depends on selectivity.&lt;/p&gt;

&lt;p&gt;PostgreSQL exposes this information through planner statistics.&lt;/p&gt;




&lt;h1&gt;
  
  
  PostgreSQL Statistics Drive Good Index Design
&lt;/h1&gt;

&lt;p&gt;For our query, PostgreSQL statistics are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;order_date [&amp;gt;=]:
    n_distinct=814
    null_frac=0.0000
    mcv_count=100
    histogram_bounds=101

customer_id [=]:
    n_distinct=89
    null_frac=0.0000
    mcv_count=89
    histogram_bounds=0

employee_id [=]:
    n_distinct=9
    null_frac=0.0000
    mcv_count=9
    histogram_bounds=0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important metric here is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;n_distinct
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Understanding &lt;code&gt;n_distinct&lt;/code&gt;
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;n_distinct&lt;/code&gt; estimates the number of distinct values in a column.&lt;/p&gt;

&lt;p&gt;Higher &lt;code&gt;n_distinct&lt;/code&gt; usually means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;higher selectivity&lt;/li&gt;
&lt;li&gt;fewer matching rows&lt;/li&gt;
&lt;li&gt;better filtering efficiency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;n_distinct&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;customer_id&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;employee_id&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;customer_id&lt;/code&gt; is significantly more selective.&lt;/p&gt;

&lt;p&gt;Therefore, PostgreSQL benefits more from filtering by &lt;code&gt;customer_id&lt;/code&gt; first.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Selectivity Matters
&lt;/h1&gt;

&lt;p&gt;Imagine the table contains 1 million rows.&lt;/p&gt;

&lt;p&gt;Filtering by:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;employee_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;may still leave:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1,000,000 / 9 ≈ 111,111 rows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Filtering by:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;may reduce the result to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1,000,000 / 89 ≈ 11,236 rows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Starting with the most selective equality predicate drastically reduces the search space.&lt;/p&gt;

&lt;p&gt;This improves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;index scan efficiency&lt;/li&gt;
&lt;li&gt;cache locality&lt;/li&gt;
&lt;li&gt;heap access reduction&lt;/li&gt;
&lt;li&gt;execution time&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  The Correct Index Design
&lt;/h1&gt;

&lt;p&gt;Applying these principles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Equality predicates first&lt;/li&gt;
&lt;li&gt;Most selective equality columns first&lt;/li&gt;
&lt;li&gt;Range predicates last&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;CONCURRENTLY&lt;/span&gt;
    &lt;span class="n"&gt;pga_idx_orders_customer_id_employee_id_order_date&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;employee_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ordering allows PostgreSQL to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate efficiently using exact matches&lt;/li&gt;
&lt;li&gt;Reduce scanned rows as early as possible&lt;/li&gt;
&lt;li&gt;Apply the range scan only after narrowing the search space&lt;/li&gt;
&lt;/ol&gt;




&lt;h1&gt;
  
  
  Good Index Design Also Depends on Table Size
&lt;/h1&gt;

&lt;p&gt;One of the biggest misconceptions about PostgreSQL optimization is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Indexes are always faster.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is false.&lt;/p&gt;

&lt;p&gt;For small tables, PostgreSQL often prefers a Sequential Scan (&lt;code&gt;Seq Scan&lt;/code&gt;) even when an index exists.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because using an index has overhead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;traversing the B-Tree&lt;/li&gt;
&lt;li&gt;reading index pages&lt;/li&gt;
&lt;li&gt;performing heap lookups&lt;/li&gt;
&lt;li&gt;random I/O access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For sufficiently small tables, scanning the entire table sequentially is cheaper.&lt;/p&gt;




&lt;h1&gt;
  
  
  Query Plans Matter More Than Theory
&lt;/h1&gt;

&lt;p&gt;A theoretically perfect index is useless if PostgreSQL never uses it.&lt;/p&gt;

&lt;p&gt;That is why index recommendation engines should never rely only on SQL syntax.&lt;/p&gt;

&lt;p&gt;They must also inspect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;execution plans&lt;/li&gt;
&lt;li&gt;estimated costs&lt;/li&gt;
&lt;li&gt;table statistics&lt;/li&gt;
&lt;li&gt;row estimates&lt;/li&gt;
&lt;li&gt;planner decisions&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  The Importance of Execution Plans
&lt;/h1&gt;

&lt;p&gt;The execution plan reveals how PostgreSQL actually executes a query.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;may show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Seq Scan on orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Index Scan using ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This distinction is critical.&lt;/p&gt;

&lt;p&gt;A query may contain filter predicates that look index-friendly, but PostgreSQL may correctly determine that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the table is too small&lt;/li&gt;
&lt;li&gt;selectivity is too low&lt;/li&gt;
&lt;li&gt;too many rows would still be scanned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and therefore prefer a sequential scan.&lt;/p&gt;




&lt;h1&gt;
  
  
  How pgAssistant Recommends Indexes
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffue2g57y0f27kpl37uot.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffue2g57y0f27kpl37uot.png" alt=" " width="800" height="620"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;pgAssistant does not simply parse SQL queries.&lt;/p&gt;

&lt;p&gt;It combines multiple sources of information:&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Query Plan
&lt;/h2&gt;

&lt;p&gt;pgAssistant analyzes nodes in the query plan to identify candidate index columns.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Predicate Types
&lt;/h2&gt;

&lt;p&gt;It classifies predicates into categories:&lt;/p&gt;

&lt;h3&gt;
  
  
  Equality predicates
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="k"&gt;IN&lt;/span&gt;
&lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Range predicates
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;
&lt;span class="k"&gt;BETWEEN&lt;/span&gt;
&lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'prefix%'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Equality predicates are prioritized before range predicates.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Column Statistics
&lt;/h2&gt;

&lt;p&gt;pgAssistant uses PostgreSQL planner statistics such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;n_distinct&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;null_frac&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;most_common_vals&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;most_common_freqs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;histogram_bounds&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;to estimate column selectivity.&lt;/p&gt;

&lt;p&gt;Columns with higher selectivity are prioritized earlier in the index definition.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Table Statistics
&lt;/h2&gt;

&lt;p&gt;pgAssistant also evaluates table-level statistics, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;estimated row counts&lt;/li&gt;
&lt;li&gt;table size&lt;/li&gt;
&lt;li&gt;planner cost estimates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is extremely important because some tables are simply too small to justify an index.&lt;/p&gt;

&lt;p&gt;In these cases, recommending an index would create unnecessary maintenance overhead without improving performance.&lt;/p&gt;




&lt;h1&gt;
  
  
  How pgAssistant Recommends PostgreSQL Indexes
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Why Query Syntax Alone Is Not Enough
&lt;/h2&gt;

&lt;p&gt;A good recommendation depends on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;predicate types&lt;/li&gt;
&lt;li&gt;column selectivity&lt;/li&gt;
&lt;li&gt;table statistics&lt;/li&gt;
&lt;li&gt;planner estimates&lt;/li&gt;
&lt;li&gt;execution plans&lt;/li&gt;
&lt;li&gt;existing indexes already used by PostgreSQL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is precisely the approach implemented by pgAssistant.&lt;/p&gt;




&lt;h1&gt;
  
  
  pgAssistant Uses Execution Plans First
&lt;/h1&gt;

&lt;p&gt;pgAssistant starts from the PostgreSQL execution plan.&lt;/p&gt;

&lt;p&gt;It analyzes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;ANALYZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FORMAT&lt;/span&gt; &lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is extremely important because the execution plan reveals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;whether PostgreSQL uses a &lt;code&gt;Seq Scan&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;whether an &lt;code&gt;Index Scan&lt;/code&gt; already exists&lt;/li&gt;
&lt;li&gt;whether residual filtering still occurs after index access&lt;/li&gt;
&lt;li&gt;whether planner row estimations are inaccurate&lt;/li&gt;
&lt;li&gt;whether a composite index could reduce heap filtering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This avoids many false-positive recommendations.&lt;/p&gt;

&lt;p&gt;A query may look index-friendly while PostgreSQL is already using the optimal access path.&lt;/p&gt;




&lt;h1&gt;
  
  
  pgAssistant Analyzes Existing Access Paths
&lt;/h1&gt;

&lt;p&gt;The advisor first determines how PostgreSQL currently accesses the table.&lt;/p&gt;

&lt;p&gt;Examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Seq Scan
Index Scan
Index Only Scan
Bitmap Heap Scan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This distinction is critical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sequential Scan Case
&lt;/h2&gt;

&lt;p&gt;If PostgreSQL performs a &lt;code&gt;Seq Scan&lt;/code&gt;, pgAssistant evaluates whether an index could realistically improve performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Indexed Access Case
&lt;/h2&gt;

&lt;p&gt;If PostgreSQL already uses an index, pgAssistant does not stop there.&lt;/p&gt;

&lt;p&gt;It also analyzes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Index Cond&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Filter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Recheck Cond&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Rows Removed by Filter&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows pgAssistant to detect situations such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Index Scan using idx_customer on orders
  Index Cond: (customer_id = 42)
  Filter: (employee_id = 5)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, PostgreSQL uses an index, but still visits many rows that are later discarded by the executor.&lt;/p&gt;

&lt;p&gt;pgAssistant can therefore recommend a more selective composite index such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;employee_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of considering the existing index “good enough”.&lt;/p&gt;




&lt;h1&gt;
  
  
  Predicate Classification
&lt;/h1&gt;

&lt;p&gt;Once predicates are extracted from the execution plan, pgAssistant classifies them by operator type.&lt;/p&gt;

&lt;p&gt;Internally, predicates are ranked according to B-Tree efficiency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Equality Predicates
&lt;/h2&gt;

&lt;p&gt;Highest priority:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="k"&gt;IN&lt;/span&gt;
&lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These predicates allow PostgreSQL to navigate directly to a very small portion of the index tree.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prefix Search Predicates
&lt;/h2&gt;

&lt;p&gt;Second priority:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'abc%'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefix searches can still benefit efficiently from B-Tree traversal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Range Predicates
&lt;/h2&gt;

&lt;p&gt;Lowest priority:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;
&lt;span class="k"&gt;BETWEEN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once PostgreSQL enters a range scan, subsequent columns become far less effective for index navigation.&lt;/p&gt;

&lt;p&gt;That is why range predicates are typically placed last in composite indexes.&lt;/p&gt;




&lt;h1&gt;
  
  
  How pgAssistant Orders Index Columns
&lt;/h1&gt;

&lt;p&gt;After classifying predicates, pgAssistant computes candidate index ordering.&lt;/p&gt;

&lt;p&gt;The internal ordering logic is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Equality predicates first
2. Prefix predicates second
3. Range predicates last
4. Inside each category:
   highest cardinality first
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This logic is implemented directly inside:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;reorder_index_candidate_columns&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The advisor therefore builds indexes that align with PostgreSQL B-Tree traversal behavior.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why Column Cardinality Matters
&lt;/h1&gt;

&lt;p&gt;pgAssistant uses PostgreSQL statistics to estimate selectivity.&lt;/p&gt;

&lt;p&gt;The most important metric is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;n_distinct
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which estimates the number of distinct values in a column.&lt;/p&gt;

&lt;p&gt;Higher cardinality usually means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fewer matching rows&lt;/li&gt;
&lt;li&gt;better filtering&lt;/li&gt;
&lt;li&gt;smaller index scan ranges&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For our example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer_id [=]: n_distinct=89
employee_id [=]: n_distinct=9
order_date [&amp;gt;=]: n_distinct=814
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although &lt;code&gt;order_date&lt;/code&gt; has the highest cardinality, it is a range predicate and therefore placed last.&lt;/p&gt;

&lt;p&gt;Among equality predicates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer_id &amp;gt; employee_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;because:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;89 &amp;gt; 9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final ordering becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;employee_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  pgAssistant Uses PostgreSQL Statistics
&lt;/h1&gt;

&lt;p&gt;pgAssistant enriches every recommendation using planner statistics extracted from PostgreSQL.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;n_distinct&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;null_frac&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;most_common_vals&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;most_common_freqs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;histogram_bounds&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The advisor even exposes these statistics in its recommendation reasoning.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customer_id [=]: n_distinct=89, null_frac=0.0000
employee_id [=]: n_distinct=9, null_frac=0.0000
order_date [&amp;gt;=]: n_distinct=814
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the recommendation transparent and explainable.&lt;/p&gt;




&lt;h1&gt;
  
  
  pgAssistant Also Uses Table Statistics
&lt;/h1&gt;

&lt;p&gt;An index is not always beneficial.&lt;/p&gt;

&lt;p&gt;This is one of the most important concepts in PostgreSQL optimization.&lt;/p&gt;

&lt;p&gt;For small tables, PostgreSQL often correctly prefers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Seq Scan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Index Scan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sequential reads are cheap&lt;/li&gt;
&lt;li&gt;index traversal has overhead&lt;/li&gt;
&lt;li&gt;heap fetches introduce random I/O&lt;/li&gt;
&lt;li&gt;scanning the entire table may cost less&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why pgAssistant also evaluates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;estimated row counts&lt;/li&gt;
&lt;li&gt;table size&lt;/li&gt;
&lt;li&gt;planner costs&lt;/li&gt;
&lt;li&gt;execution frequency&lt;/li&gt;
&lt;li&gt;workload intensity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The advisor does not blindly recommend indexes whenever a sequential scan appears.&lt;/p&gt;




&lt;h1&gt;
  
  
  Detecting Inefficient Indexed Access
&lt;/h1&gt;

&lt;p&gt;One particularly powerful aspect of pgAssistant is its ability to analyze residual filtering.&lt;/p&gt;

&lt;p&gt;For indexed scans, the advisor evaluates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rows Removed by Filter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If PostgreSQL retrieves many tuples from the index only to discard them afterward, pgAssistant detects that the current index may be incomplete.&lt;/p&gt;

&lt;p&gt;Internally, the advisor computes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Residual filter kept X% of tuples visited
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helps identify situations where adding an additional column to a composite index could drastically reduce heap filtering.&lt;/p&gt;




&lt;h1&gt;
  
  
  Detecting Planner Estimation Problems
&lt;/h1&gt;

&lt;p&gt;pgAssistant also compares:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plan_rows
vs
actual_rows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Large estimation gaps may indicate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stale statistics&lt;/li&gt;
&lt;li&gt;data skew&lt;/li&gt;
&lt;li&gt;correlation issues&lt;/li&gt;
&lt;li&gt;missing extended statistics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This additional analysis improves the reliability of recommendations.&lt;/p&gt;




&lt;h1&gt;
  
  
  The pgAssistant Recommendation Algorithm
&lt;/h1&gt;

&lt;p&gt;Conceptually, pgAssistant follows this workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Analyze EXPLAIN ANALYZE JSON plan
2. Detect access paths
3. Extract predicates from:
   - Index Cond
   - Filter
   - Recheck Cond
4. Classify predicates by operator type
5. Rank predicates for B-Tree efficiency
6. Use PostgreSQL statistics to estimate selectivity
7. Order columns by:
   - predicate class
   - cardinality
8. Evaluate table statistics
9. Evaluate execution costs
10. Detect residual filtering
11. Compare against existing indexes
12. Recommend index only if beneficial
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Example: Final Recommendation
&lt;/h1&gt;

&lt;p&gt;Given:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;employee_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;order_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ship_country&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;employee_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;order_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;pgAssistant evaluates:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column&lt;/th&gt;
&lt;th&gt;Predicate&lt;/th&gt;
&lt;th&gt;Priority&lt;/th&gt;
&lt;th&gt;n_distinct&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;customer_id&lt;/td&gt;
&lt;td&gt;&lt;code&gt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Equality&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;employee_id&lt;/td&gt;
&lt;td&gt;&lt;code&gt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Equality&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;order_date&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Range&lt;/td&gt;
&lt;td&gt;814&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The advisor therefore generates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;CONCURRENTLY&lt;/span&gt;
    &lt;span class="nv"&gt;"pga_idx_orders_customer_id_employee_id_order_date"&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="nv"&gt;"public"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"orders"&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"customer_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"employee_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"order_date"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ordering follows PostgreSQL B-Tree optimization principles while also considering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;planner statistics&lt;/li&gt;
&lt;li&gt;table characteristics&lt;/li&gt;
&lt;li&gt;execution plan behavior&lt;/li&gt;
&lt;li&gt;residual filtering&lt;/li&gt;
&lt;li&gt;existing indexes already in use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Live demo&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A public demo is available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ov-004f8b.infomaniak.ch/" rel="noopener noreferrer"&gt;https://ov-004f8b.infomaniak.ch/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Demo connection:&lt;/p&gt;

&lt;p&gt;postgresql://postgres:demo@demo-db:5432/northwind&lt;/p&gt;

&lt;p&gt;The public demo intentionally runs without AI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project links&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/beh74/pgassistant-community" rel="noopener noreferrer"&gt;https://github.com/beh74/pgassistant-community&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Documentation: &lt;a href="https://beh74.github.io/pgassistant-blog/" rel="noopener noreferrer"&gt;https://beh74.github.io/pgassistant-blog/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docker image: &lt;a href="https://hub.docker.com/r/bertrand73/pgassistant" rel="noopener noreferrer"&gt;https://hub.docker.com/r/bertrand73/pgassistant&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Feedback welcome&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The project is still evolving and many parts can certainly be improved.&lt;/p&gt;

&lt;p&gt;If you work with PostgreSQL and have ideas, feedback, or criticisms, feel free to open an issue or discussion on GitHub.&lt;/p&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>performance</category>
      <category>postgressql</category>
    </item>
    <item>
      <title>pgAssistant 2.8 — Deterministic PostgreSQL Analysis with the new Global Advisor</title>
      <dc:creator>bertrand HARTWIG</dc:creator>
      <pubDate>Fri, 08 May 2026 06:01:15 +0000</pubDate>
      <link>https://dev.to/bertrand_hartwig_309d1958/pgassistant-28-deterministic-postgresql-analysis-with-the-new-global-advisor-ig4</link>
      <guid>https://dev.to/bertrand_hartwig_309d1958/pgassistant-28-deterministic-postgresql-analysis-with-the-new-global-advisor-ig4</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8vzi2hozpxzikvsrko15.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8vzi2hozpxzikvsrko15.png" alt="Global Advisor" width="800" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the past months, I have been working on a simple idea around PostgreSQL tooling:&lt;/p&gt;

&lt;p&gt;before using AI, start with deterministic analysis.&lt;/p&gt;

&lt;p&gt;This is the direction behind &lt;strong&gt;pgAssistant 2.8&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This release introduces a new component called Global Advisor, alongside many improvements around ranking, schema analysis, maintenance diagnostics, and index recommendations.&lt;/p&gt;

&lt;p&gt;The project remains open-source and focused on practical PostgreSQL analysis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is pgAssistant?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;pgAssistant is an open-source PostgreSQL analysis tool.&lt;/p&gt;

&lt;p&gt;It helps developers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inspect database structures&lt;/li&gt;
&lt;li&gt;analyze execution plans&lt;/li&gt;
&lt;li&gt;detect schema and maintenance issues&lt;/li&gt;
&lt;li&gt;review indexes and foreign keys&lt;/li&gt;
&lt;li&gt;understand PostgreSQL behavior more easily&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project combines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deterministic analysis&lt;/li&gt;
&lt;li&gt;execution-plan analysis (EXPLAIN ANALYZE)&lt;/li&gt;
&lt;li&gt;optional AI-assisted reasoning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to replace PostgreSQL expertise.&lt;br&gt;
The goal is simply to make PostgreSQL diagnostics more accessible and more contextual.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The main addition in 2.8: Global Advisor&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before pgAssistant 2.8, most checks existed independently.&lt;/p&gt;

&lt;p&gt;Now they are consolidated into a single entry point:&lt;/p&gt;

&lt;p&gt;Global Advisor&lt;/p&gt;

&lt;p&gt;The Global Advisor performs a database-wide deterministic analysis and aggregates findings into a unified recommendation list.&lt;/p&gt;

&lt;p&gt;Each recommendation now includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a rank&lt;/li&gt;
&lt;li&gt;a confidence score&lt;/li&gt;
&lt;li&gt;an estimated impact&lt;/li&gt;
&lt;li&gt;an estimated implementation effort&lt;/li&gt;
&lt;li&gt;a suggested SQL statement when relevant&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5wtwlmm1kr3jvkxv78jr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5wtwlmm1kr3jvkxv78jr.png" alt="Global Advisor" width="800" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The objective is not to claim certainty.&lt;/p&gt;

&lt;p&gt;It is to help prioritize investigations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deterministic first&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One important design choice in pgAssistant is that the Global Advisor is intentionally deterministic.&lt;/p&gt;

&lt;p&gt;The analysis is based directly on PostgreSQL catalogs and statistics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pg_stat_user_tables&lt;/li&gt;
&lt;li&gt;pg_stat_user_indexes&lt;/li&gt;
&lt;li&gt;pg_constraint&lt;/li&gt;
&lt;li&gt;pg_index&lt;/li&gt;
&lt;li&gt;pg_settings&lt;/li&gt;
&lt;li&gt;pg_stats&lt;/li&gt;
&lt;li&gt;execution plans&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;same input → same output&lt;/li&gt;
&lt;li&gt;no hallucinations&lt;/li&gt;
&lt;li&gt;explainable findings&lt;/li&gt;
&lt;li&gt;reproducible analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI is still supported as an optional layer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzlg8zpf1fk1y2iap2ggw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzlg8zpf1fk1y2iap2ggw.png" alt="Query Advisor" width="800" height="532"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Examples of checks now included&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Global Advisor currently includes checks such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;missing indexes on foreign keys&lt;/li&gt;
&lt;li&gt;redundant or duplicate indexes&lt;/li&gt;
&lt;li&gt;unused indexes&lt;/li&gt;
&lt;li&gt;invalid indexes&lt;/li&gt;
&lt;li&gt;datatype inconsistencies on foreign keys&lt;/li&gt;
&lt;li&gt;tables without primary keys&lt;/li&gt;
&lt;li&gt;stale statistics&lt;/li&gt;
&lt;li&gt;tables never vacuumed&lt;/li&gt;
&lt;li&gt;estimated table bloat&lt;/li&gt;
&lt;li&gt;excessive index-to-table ratio&lt;/li&gt;
&lt;li&gt;low foreign key coverage&lt;/li&gt;
&lt;li&gt;PostgreSQL configuration checks&lt;/li&gt;
&lt;li&gt;sequences approaching exhaustion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most recommendations also include suggested SQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query analysis is still there&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The query advisor based on real EXPLAIN ANALYZE plans remains a core part of pgAssistant.&lt;/p&gt;

&lt;p&gt;The idea is now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global Advisor → broad database analysis&lt;/li&gt;
&lt;li&gt;Query Advisor → detailed query-level investigation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These two approaches complement each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About AI&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI support remains optional.&lt;/p&gt;

&lt;p&gt;pgAssistant can work entirely without an LLM.&lt;/p&gt;

&lt;p&gt;When enabled, AI features receive contextual PostgreSQL information:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;schema definitions&lt;/li&gt;
&lt;li&gt;indexes&lt;/li&gt;
&lt;li&gt;execution plans&lt;/li&gt;
&lt;li&gt;statistics&lt;/li&gt;
&lt;li&gt;database settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This significantly improves the relevance of generated suggestions compared to generic SQL prompting.&lt;/p&gt;

&lt;p&gt;Supported providers currently include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ollama&lt;/li&gt;
&lt;li&gt;OpenAI-compatible APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why I built it this way&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A lot of PostgreSQL tooling focuses on metrics dashboards.&lt;/p&gt;

&lt;p&gt;Those tools are useful, but I often felt there was still a gap between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;seeing a metric&lt;/li&gt;
&lt;li&gt;understanding the cause&lt;/li&gt;
&lt;li&gt;deciding what to change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;pgAssistant tries to reduce that gap.&lt;/p&gt;

&lt;p&gt;The project is still evolving, but the Global Advisor is an important step toward a more coherent analysis workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A public demo is available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ov-004f8b.infomaniak.ch/" rel="noopener noreferrer"&gt;https://ov-004f8b.infomaniak.ch/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Demo connection:&lt;/p&gt;

&lt;p&gt;postgresql://postgres:demo@demo-db:5432/northwind&lt;/p&gt;

&lt;p&gt;The public demo intentionally runs without AI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project links&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/beh74/pgassistant-community" rel="noopener noreferrer"&gt;https://github.com/beh74/pgassistant-community&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Documentation: &lt;a href="https://beh74.github.io/pgassistant-blog/" rel="noopener noreferrer"&gt;https://beh74.github.io/pgassistant-blog/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docker image: &lt;a href="https://hub.docker.com/r/bertrand73/pgassistant" rel="noopener noreferrer"&gt;https://hub.docker.com/r/bertrand73/pgassistant&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Feedback welcome&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The project is still evolving and many parts can certainly be improved.&lt;/p&gt;

&lt;p&gt;If you work with PostgreSQL and have ideas, feedback, or criticisms, feel free to open an issue or discussion on GitHub.&lt;/p&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;

</description>
      <category>database</category>
      <category>opensource</category>
      <category>postgres</category>
      <category>tooling</category>
    </item>
    <item>
      <title>pgAssistant v1.7 released</title>
      <dc:creator>bertrand HARTWIG</dc:creator>
      <pubDate>Sat, 01 Feb 2025 13:09:19 +0000</pubDate>
      <link>https://dev.to/bertrand_hartwig_309d1958/pgassistant-v17-released-3309</link>
      <guid>https://dev.to/bertrand_hartwig_309d1958/pgassistant-v17-released-3309</guid>
      <description>&lt;p&gt;I'm excited to share that we just released &lt;strong&gt;pgAssistant&lt;/strong&gt; v1.7.&lt;/p&gt;

&lt;p&gt;PGAssistant is an open-source tool designed to help &lt;strong&gt;developers&lt;/strong&gt; gain deeper insights into their PostgreSQL databases and optimize performance efficiently.&lt;/p&gt;

&lt;p&gt;It analyzes database behavior, detects schema-related issues, and provides actionable recommendations to resolve them.&lt;/p&gt;

&lt;p&gt;One of the goals of PGAssistant is to help developers optimize their database and fix potential issues on their own before needing to seek assistance from a DBA.&lt;/p&gt;

&lt;p&gt;🚀 &lt;strong&gt;AI-Powered Optimization:&lt;/strong&gt; PGAssistant leverages AI-driven language models like ChatGPT, Claude, and on-premise solutions such as Ollama to assist developers in refining complex queries and enhancing database efficiency.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/nexsol-technologies/pgassistant" rel="noopener noreferrer"&gt;PGAssistant&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🚀 &lt;strong&gt;Easy Deployment with Docker:&lt;/strong&gt; PGAssistant is Docker-based, making it simple to run. Get started effortlessly using the provided &lt;a href="https://github.com/nexsol-technologies/pgassistant/blob/main/docker-compose/docker-compose.yml" rel="noopener noreferrer"&gt;Docker Compose file&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I’d love to hear your feedback! If you find PGAssistant useful, feel free to contribute or suggest new features. Let’s make PostgreSQL database easy for dev Teams !&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>postgressql</category>
      <category>productivity</category>
      <category>ai</category>
    </item>
    <item>
      <title>pgAssistant - postgesql tool 4 dev</title>
      <dc:creator>bertrand HARTWIG</dc:creator>
      <pubDate>Mon, 12 Aug 2024 05:00:03 +0000</pubDate>
      <link>https://dev.to/bertrand_hartwig_309d1958/pgassistant-postgesql-tool-4-dev-16cp</link>
      <guid>https://dev.to/bertrand_hartwig_309d1958/pgassistant-postgesql-tool-4-dev-16cp</guid>
      <description>&lt;p&gt;I wrote this small tool because I noticed that our young developers were struggling to understand the behavior of their PostgreSQL database, and therefore had even more difficulty optimizing their databases. &lt;/p&gt;

&lt;p&gt;Gradually, developers started using it, and the tool helped the development teams become much more autonomous, and me much less solicited. &lt;/p&gt;

&lt;p&gt;Feel free to use it and give me your feedback. &lt;/p&gt;

&lt;p&gt;I try to enhance the application whenever time allows.&lt;/p&gt;

&lt;p&gt;You can find it there : &lt;a href="https://github.com/nexsol-technologies/pgassistant" rel="noopener noreferrer"&gt;https://github.com/nexsol-technologies/pgassistant&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>postgres</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
