<?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: Ariel Bodan</title>
    <description>The latest articles on DEV Community by Ariel Bodan (@bodanthebackend).</description>
    <link>https://dev.to/bodanthebackend</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%2F4041005%2Ffd883491-b631-4f00-9489-528005a9be0a.jpg</url>
      <title>DEV Community: Ariel Bodan</title>
      <link>https://dev.to/bodanthebackend</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bodanthebackend"/>
    <language>en</language>
    <item>
      <title>PostgreSQL LISTEN/NOTIFY Queue Full: A 512 KiB Reproduction</title>
      <dc:creator>Ariel Bodan</dc:creator>
      <pubDate>Sat, 19 Sep 2026 01:46:01 +0000</pubDate>
      <link>https://dev.to/bodanthebackend/postgresql-listennotify-queue-full-a-512-kib-reproduction-43g0</link>
      <guid>https://dev.to/bodanthebackend/postgresql-listennotify-queue-full-a-512-kib-reproduction-43g0</guid>
      <description>&lt;h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg5li447i01i7xav978o4.jpg" alt=" " width="799" height="436"&gt;
&lt;/h2&gt;

&lt;p&gt;What causes PostgreSQL to report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: too many notifications in the NOTIFY queue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I reproduced it with PostgreSQL 17, Docker, and two &lt;code&gt;psql&lt;/code&gt; sessions.&lt;/p&gt;

&lt;p&gt;Instead of trying to fill PostgreSQL's default multi-gigabyte queue, I started the server with:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;With 8 KiB pages, that gave the experiment a queue capacity of only &lt;strong&gt;512 KiB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The listener did this:&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;LISTEN&lt;/span&gt; &lt;span class="n"&gt;demo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and remained &lt;code&gt;idle in transaction&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From another connection, I generated 100 large &lt;code&gt;pg_notify()&lt;/code&gt; calls.&lt;/p&gt;

&lt;p&gt;The result was easy to observe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Successful sends: 64
Failed sends:     36
Queue usage:      100%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: too many notifications in the NOTIFY queue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I returned to the listener and executed:&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;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pending notifications were delivered and:&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;pg_notification_queue_usage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;returned to &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The useful takeaway is that increasing &lt;code&gt;max_notify_queue_pages&lt;/code&gt; only gives the system more space. It does not remove a transaction that is preventing notification cleanup.&lt;/p&gt;

&lt;p&gt;The full experiment includes the Docker Compose setup, SQL commands, queue measurements, inspection of &lt;code&gt;pg_notify/&lt;/code&gt;, and recovery verification:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bodanthebackend.com/articles/en/postgresql-listen-notify-queue-full/" rel="noopener noreferrer"&gt;https://bodanthebackend.com/articles/en/postgresql-listen-notify-queue-full/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>postgres</category>
      <category>programming</category>
      <category>backend</category>
    </item>
    <item>
      <title>How I Turned 350+ Programming Books Into 24 Learning Roadmaps With AI</title>
      <dc:creator>Ariel Bodan</dc:creator>
      <pubDate>Fri, 18 Sep 2026 06:10:45 +0000</pubDate>
      <link>https://dev.to/bodanthebackend/how-i-turned-350-programming-books-into-24-learning-roadmaps-with-ai-3j0e</link>
      <guid>https://dev.to/bodanthebackend/how-i-turned-350-programming-books-into-24-learning-roadmaps-with-ai-3j0e</guid>
      <description>&lt;p&gt;I had more than 350 programming books and resources sitting on my computer.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsi5ijeremrz4ugjdz8si.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsi5ijeremrz4ugjdz8si.png" alt=" " width="800" height="342"&gt;&lt;/a&gt;&lt;br&gt;
Some algorithm books were loose in the root folder. A JavaScript book somehow ended up inside Docker. Arduino material was mixed with SEO resources. I also had duplicate copies of books spread across different folders.&lt;/p&gt;

&lt;p&gt;It was a mess.&lt;/p&gt;

&lt;p&gt;But the bigger problem wasn't the folder structure.&lt;/p&gt;

&lt;p&gt;I knew there were plenty of good books in there. I just had no idea what I should read first.&lt;/p&gt;

&lt;p&gt;Backend? Data Engineering? Advanced Python? C?&lt;/p&gt;

&lt;p&gt;Everything was mixed together.&lt;/p&gt;

&lt;p&gt;So instead of spending an entire Saturday moving files around by hand, I decided to see how far I could get with opencode.&lt;/p&gt;

&lt;p&gt;I started with a simple request&lt;/p&gt;

&lt;p&gt;I asked it to scan the whole folder recursively, group the files by topic, and create a learning roadmap for each topic.&lt;/p&gt;

&lt;p&gt;My original prompt was basically:&lt;/p&gt;

&lt;p&gt;Review the folder recursively, organize the files by topic, and create a roadmap for each topic — C Programming, Backend Engineering, Data Engineering, Data Analytics, and so on — in a Markdown file.&lt;/p&gt;

&lt;p&gt;I expected a cleaned-up list of books.&lt;/p&gt;

&lt;p&gt;What I got was much more useful.&lt;/p&gt;

&lt;p&gt;It created 24 different learning areas, each with its own roadmap.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Froeszy5ic2bbqu8owmvb.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Froeszy5ic2bbqu8owmvb.png" alt=" " width="800" height="930"&gt;&lt;/a&gt;&lt;br&gt;
Instead of just grouping the books into folders, it organized them into stages:&lt;/p&gt;

&lt;p&gt;Foundations → Intermediate → Advanced&lt;/p&gt;

&lt;p&gt;Each book was placed where it made the most sense in the learning path.&lt;/p&gt;

&lt;p&gt;So if I want to learn C from scratch, I can see which book to start with.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnqyck4qpi0tzfqsq7z68.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnqyck4qpi0tzfqsq7z68.png" alt=" " width="800" height="880"&gt;&lt;/a&gt;&lt;br&gt;
If I want to focus on Data Engineering, there's a progression for that too.&lt;/p&gt;

&lt;p&gt;If I'm preparing for technical interviews, I have a separate path.&lt;/p&gt;

&lt;p&gt;That was the point where the project stopped being about file organization.&lt;/p&gt;

&lt;p&gt;It became a way to answer a much better question:&lt;/p&gt;

&lt;p&gt;What should I study next?&lt;/p&gt;

&lt;p&gt;Then I let it reorganize the actual files&lt;/p&gt;

&lt;p&gt;Once the roadmaps looked good, I asked it to make the real folder structure match them.&lt;/p&gt;

&lt;p&gt;This was the part where I wanted to be more careful.&lt;/p&gt;

&lt;p&gt;Moving hundreds of files is one thing. Deleting the wrong file is another.&lt;/p&gt;

&lt;p&gt;So I had it:&lt;/p&gt;

&lt;p&gt;create new folders where needed;&lt;br&gt;
move books into the right topic;&lt;br&gt;
find duplicate files;&lt;br&gt;
compare duplicates by their actual contents, not just their filenames;&lt;br&gt;
verify the total file count before and after;&lt;br&gt;
update the roadmap with the new file paths.&lt;/p&gt;

&lt;p&gt;It found four exact duplicates I didn't even know I had.&lt;/p&gt;

&lt;p&gt;After everything was reorganized, I ended up with:&lt;/p&gt;

&lt;p&gt;348 resources organized across 24 topics.&lt;/p&gt;

&lt;p&gt;And, more importantly, nothing got lost.&lt;/p&gt;

&lt;p&gt;The prompt I'd use today&lt;/p&gt;

&lt;p&gt;The first prompt worked, but I'd make it more specific now:&lt;/p&gt;

&lt;p&gt;Scan my programming library recursively, including subfolders and PDF/EPUB files. Classify every file by technical topic, such as C Programming, Backend Engineering, Data Engineering, Data Analytics, Frontend, DevOps, AI, and others.&lt;/p&gt;

&lt;p&gt;For each topic, create a learning roadmap divided into stages: foundations, intermediate, and advanced. Place each resource in the stage where it's most useful.&lt;/p&gt;

&lt;p&gt;Save everything in a ROADMAPS_BY_TOPIC.md file and include the goal of each stage. At the end, tell me what percentage of the files were successfully categorized.&lt;/p&gt;

&lt;p&gt;For the actual reorganization, I'd also add:&lt;/p&gt;

&lt;p&gt;Before moving anything, show me the planned file changes in a table.&lt;/p&gt;

&lt;p&gt;Only remove exact duplicates after comparing their contents. Never assume two files are duplicates just because their names are similar.&lt;/p&gt;

&lt;p&gt;When you're done, verify that the file count only changed because exact duplicates were removed, and update all paths in the roadmap.&lt;/p&gt;

&lt;p&gt;That extra approval step is worth having whenever an AI agent is going to make real changes to your files.&lt;/p&gt;

&lt;p&gt;What I'm doing next&lt;/p&gt;

&lt;p&gt;My next step is to create a checklist for each topic with:&lt;/p&gt;

&lt;p&gt;books I've finished;&lt;br&gt;
books I still need to read;&lt;br&gt;
notes;&lt;br&gt;
progress through the roadmap.&lt;/p&gt;

&lt;p&gt;That would turn the whole library into something closer to a personal learning system instead of a folder full of things I might read someday.&lt;/p&gt;

&lt;p&gt;I wrote up the full process, including the original prompts and the improved versions, here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bodanthebackend.com/articles/en/organizar-biblioteca-programacion-ia-opencode/" rel="noopener noreferrer"&gt;https://bodanthebackend.com/articles/en/organizar-biblioteca-programacion-ia-opencode/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm curious if anyone else has tried using an AI agent for something similar — not just coding, but organizing the resources you use to learn.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>learning</category>
    </item>
    <item>
      <title>Why Adding an Index Won't Fix Your Slow COUNT(*) in PostgreSQL</title>
      <dc:creator>Ariel Bodan</dc:creator>
      <pubDate>Mon, 07 Sep 2026 03:15:00 +0000</pubDate>
      <link>https://dev.to/bodanthebackend/why-adding-an-index-wont-fix-your-slow-count-in-postgresql-477a</link>
      <guid>https://dev.to/bodanthebackend/why-adding-an-index-wont-fix-your-slow-count-in-postgresql-477a</guid>
      <description>&lt;p&gt;&lt;code&gt;COUNT(*)&lt;/code&gt; looks like a trivial operation:&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="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query asks for a single number, but that doesn't mean PostgreSQL can produce it with a constant-time read from some internal counter.&lt;/p&gt;

&lt;p&gt;When we need an exact count, PostgreSQL has to determine how many rows are actually part of the visible result set for that query. On large tables, that work can become a meaningful chunk of total execution time.&lt;/p&gt;

&lt;p&gt;And the problem doesn't just go away by throwing an index at it.&lt;/p&gt;

&lt;p&gt;The useful question isn't "do I have an index?" It's:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many rows does PostgreSQL actually need to examine to compute this count — and can that work be reduced?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why COUNT(*) Can Be Expensive in PostgreSQL
&lt;/h2&gt;

&lt;p&gt;PostgreSQL uses MVCC — Multi-Version Concurrency Control — to manage concurrent access to data.&lt;/p&gt;

&lt;p&gt;That's what lets multiple transactions work at the same time while each sees a consistent view of the database. But it also means row visibility depends on the snapshot the query is running under.&lt;/p&gt;

&lt;p&gt;That's why PostgreSQL can't answer:&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="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;by simply reading an exact counter stored somewhere in the table's metadata.&lt;/p&gt;

&lt;p&gt;To return an exact result, it has to process the rows — or an index structure representing those rows — and determine which ones are part of the visible result.&lt;/p&gt;

&lt;p&gt;On a small table, that cost is invisible.&lt;/p&gt;

&lt;p&gt;On a table with millions of rows, the amount of work starts to matter.&lt;/p&gt;

&lt;p&gt;Which leads to an important distinction:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;returning a single row from &lt;code&gt;COUNT(*)&lt;/code&gt; does not mean processing a single row.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Analyze a COUNT with EXPLAIN ANALYZE
&lt;/h2&gt;

&lt;p&gt;Before reaching for an index, it's worth looking at what PostgreSQL is actually doing.&lt;/p&gt;

&lt;p&gt;Say we have this 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="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can analyze it with:&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;BUFFERS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal isn't to hunt for an &lt;code&gt;Index Scan&lt;/code&gt; by default. Worth checking instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the scan type&lt;/li&gt;
&lt;li&gt;estimated rows vs. actual rows processed&lt;/li&gt;
&lt;li&gt;rows discarded by the filter&lt;/li&gt;
&lt;li&gt;buffer activity&lt;/li&gt;
&lt;li&gt;total execution time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostgreSQL might well choose:&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;if it estimates that a sequential read is cheaper than using an index. That doesn't mean the planner made a bad call — it depends entirely on how many rows match the condition.&lt;/p&gt;

&lt;h2&gt;
  
  
  When an Index Can Actually Improve a COUNT
&lt;/h2&gt;

&lt;p&gt;Say we have 10 million orders, but only 30,000 are &lt;code&gt;pending&lt;/code&gt;:&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="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the filter shrinks the working set meaningfully. An index on &lt;code&gt;status&lt;/code&gt; could let PostgreSQL skip most of the table:&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;idx_orders_status&lt;/span&gt;
&lt;span class="k"&gt;ON&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;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on statistics, value distribution, and page state, PostgreSQL might use an index-based access path — and under the right conditions, an &lt;code&gt;Index Only Scan&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But it's important not to treat that as a guarantee:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;having an index does not guarantee an &lt;code&gt;Index Only Scan&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The planner picks whatever it estimates as the cheapest plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Index Only Scan and the Visibility Map
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;Index Only Scan&lt;/code&gt; can skip a lot of table visits because the values needed to answer the query already live in the index itself.&lt;/p&gt;

&lt;p&gt;For a count like:&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="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;an index on &lt;code&gt;status&lt;/code&gt; holds what's needed to locate the relevant entries. But PostgreSQL still has to respect MVCC's visibility rules — and that's where the &lt;strong&gt;visibility map&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;PostgreSQL tracks which pages contain only rows that are visible to every relevant transaction. When a page is marked &lt;code&gt;all-visible&lt;/code&gt;, an &lt;code&gt;Index Only Scan&lt;/code&gt; can skip visiting the heap to check row visibility individually.&lt;/p&gt;

&lt;p&gt;This is why it's wrong to think &lt;code&gt;VACUUM&lt;/code&gt; "updates the index." The index already stays current as the table changes. What &lt;code&gt;VACUUM&lt;/code&gt; actually helps maintain is the visibility information that lets PostgreSQL avoid certain heap visits.&lt;/p&gt;

&lt;p&gt;You can check this directly in the plan by looking at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Heap Fetches: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A high &lt;code&gt;Heap Fetches&lt;/code&gt; count means PostgreSQL had to check table pages to confirm visibility, even while using an &lt;code&gt;Index Only Scan&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Problem with COUNT and Filters: Selectivity
&lt;/h2&gt;

&lt;p&gt;Now flip the scenario. Say 90% of orders are &lt;code&gt;completed&lt;/code&gt;:&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="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In that case, an index on &lt;code&gt;status&lt;/code&gt; buys you a lot less. Why? Because even if PostgreSQL can quickly locate every entry where &lt;code&gt;status = completed&lt;/code&gt;, those entries make up almost the entire table.&lt;/p&gt;

&lt;p&gt;The index isn't eliminating enough work.&lt;/p&gt;

&lt;p&gt;That's why PostgreSQL might still prefer a &lt;code&gt;Seq Scan&lt;/code&gt; even when there's an index that appears to match the &lt;code&gt;WHERE&lt;/code&gt; clause.&lt;/p&gt;

&lt;p&gt;The right question was never:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does an index exist?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How much does that index actually shrink the set of data PostgreSQL has to process?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Selectivity matters more than the mere existence of an index.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a Partial Index Makes Sense
&lt;/h2&gt;

&lt;p&gt;Partial indexes shine when you're frequently querying a small, well-defined subset of data.&lt;/p&gt;

&lt;p&gt;Say only a small fraction of orders are &lt;code&gt;pending&lt;/code&gt;:&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="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&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;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We could create:&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;idx_orders_pending&lt;/span&gt;
&lt;span class="k"&gt;ON&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;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That index doesn't contain every order — only the ones matching &lt;code&gt;status = 'pending'&lt;/code&gt;. If &lt;code&gt;pending&lt;/code&gt; is a small subset, this index can end up considerably smaller than one covering the whole table, and for queries using exactly that predicate, PostgreSQL works with a much leaner structure.&lt;/p&gt;

&lt;p&gt;The math changes completely if you build a partial index around a value that represents 90% of the table — you'd still be maintaining an index covering nearly every row, so the potential savings shrink accordingly.&lt;/p&gt;

&lt;p&gt;Partial indexes pay off when they mirror a real, sufficiently selective access pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  COUNT with Multiple Filters
&lt;/h2&gt;

&lt;p&gt;Counts get more interesting once they combine several conditions:&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="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&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;account_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;created_at&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="s1"&gt;'2026-09-01'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single-column index may not be enough here. Worth exploring a composite 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="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_account_status_created&lt;/span&gt;
&lt;span class="k"&gt;ON&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;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But don't just copy that pattern without measuring. Column order should reflect how the data is actually filtered, its distribution, and the app's real query patterns.&lt;/p&gt;

&lt;p&gt;The right workflow is still:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query
↓
EXPLAIN ANALYZE
↓
rows processed
↓
selectivity
↓
index design
↓
new EXPLAIN ANALYZE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;slow query
↓
create index
↓
hope
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When an Index Won't Fix the COUNT
&lt;/h2&gt;

&lt;p&gt;There's a hard limit here. If you genuinely need to count a large chunk of a huge table, PostgreSQL has to process a substantial amount of data to produce an exact result.&lt;/p&gt;

&lt;p&gt;An index can change &lt;em&gt;how&lt;/em&gt; that data is accessed. It can't make the rows that belong in the count disappear.&lt;/p&gt;

&lt;p&gt;If an application is constantly running:&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="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;against a very large table and expects a near-instant response, it's worth asking whether running an exact count on every single request is even the right model. That's where other strategies come in — precomputed counters, approximate counts via &lt;code&gt;pg_class.reltuples&lt;/code&gt;, and a practical checklist for diagnosing any slow COUNT.&lt;/p&gt;

&lt;h2&gt;
  
  
  I go through all of that — plus the full 10-step diagnostic checklist — in the &lt;a href="https://bodanthebackend.com/articles/en/postgresql-optimize-count-query/" rel="noopener noreferrer"&gt;complete write-up on my site&lt;/a&gt;.
&lt;/h2&gt;

&lt;p&gt;Curious how others handle this in production: do you maintain precomputed counters, or just accept the cost of an exact COUNT when it matters? Would love to hear how you've dealt with this in your own stack.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>sql</category>
      <category>database</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
