<?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>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>
