<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>boringSQL | Supercharge your SQL &amp; PostgreSQL powers - indexes</title>
    <subtitle>Learn practical SQL &amp; PostgreSQL techniques. Build rock-solid data systems with &#x27;boring&#x27; database solutions that deliver reliability without the drama.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://boringsql.com/tags/indexes/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://boringsql.com"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2024-04-14T00:00:00+00:00</updated>
    <id>https://boringsql.com/tags/indexes/atom.xml</id>
    <entry xml:lang="en">
        <title>When and Why PostgreSQL Indexes Are Ignored</title>
        <published>2024-04-14T00:00:00+00:00</published>
        <updated>2024-04-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Radim Marek
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://boringsql.com/posts/why-postgresql-indexes-are-ignored/"/>
        <id>https://boringsql.com/posts/why-postgresql-indexes-are-ignored/</id>
        
        <content type="html" xml:base="https://boringsql.com/posts/why-postgresql-indexes-are-ignored/">&lt;p&gt;While it&#x27;s true the most problems can be solved by the appropriate use of the index, there are cases where you will just waste resources doing so. For casual developer it might seems like PostgreSQL decided to do its own thing, but when you look behind the scenes it all makes perfect sense. Here&#x27;s a quick run down of the some reasons why planner might pass on index and rather do things without it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-case-of-missing-condition&quot;&gt;The case of missing condition&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-case-of-missing-condition&quot; aria-label=&quot;Anchor link for: the-case-of-missing-condition&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;In the evolution of the software developer journey into the realms of databases, the partial indexes are the next best thing. Why to create the full index where you can easily restrict it to the sub-set of the records? The trick is ensure every query aligns with the condition given during the index creation.&lt;&#x2F;p&gt;
&lt;p&gt;Think of an order management system, where most active orders are going to those in &#x27;open&#x27; status. Most of the day-to-day operational tasks will resolve around those entries, a partial index like&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; open_orders&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; orders(order_id) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE state =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;open&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;provides a perfect match. It will allow to iterate only on a relatively small subset of the data, hence saving the disc space. As long as the application developers are aware of the partial condition, and how to use it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-column-order-matters&quot;&gt;The column order matters&lt;a class=&quot;zola-anchor&quot; href=&quot;#the-column-order-matters&quot; aria-label=&quot;Anchor link for: the-column-order-matters&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;The second common case is just a step away from the partial index. The compound indexes, or multi-column indexes, are versatile and powerful - that is when used correctly. The crucial element is the order in which the columns are indexed. The order impacts whatever it gets used or not at all.&lt;&#x2F;p&gt;
&lt;p&gt;The key is to match the left-most columns of the index in your queries. Once these are aligned, you can optionally skip or include additional columns in the filter, but the initial columns must be used to leverage the index effectively.&lt;&#x2F;p&gt;
&lt;p&gt;While it might be easy not to miss the condition of the country, when filtering for cities&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; contact_location&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; contact_details (country_id, city);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;consider the scenario which might not be so obvious.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; brand_products_per_category&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; products (category_id, brand_id);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;In this case the index will only get used if either both &lt;code&gt;category_id&lt;&#x2F;code&gt; and &lt;code&gt;brand_id&lt;&#x2F;code&gt; are used, or at least &lt;code&gt;category_id&lt;&#x2F;code&gt; - but not for &lt;code&gt;brand_id&lt;&#x2F;code&gt; alone. In latter case PostgreSQL planner might (unless another index is available) to opt in for scan of the entire table.&lt;&#x2F;p&gt;
&lt;p&gt;In this case alternative strategy would be to switch the column order (should the application logic support it).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;low-selectivity&quot;&gt;Low Selectivity&lt;a class=&quot;zola-anchor&quot; href=&quot;#low-selectivity&quot; aria-label=&quot;Anchor link for: low-selectivity&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;If we use the example of the partial index from the first section, we can easily demonstrate another case when index just might get ignored forever. It&#x27;s the case for the columns with low selectivity, where a predominant value overshadows others, making an index less useful.&lt;&#x2F;p&gt;
&lt;p&gt;Take example of the ordering system. While having index for &lt;code&gt;open&lt;&#x2F;code&gt; orders is helpful, in most scenarios majority of the records will end up in &lt;code&gt;delivered&lt;&#x2F;code&gt; state. Hence&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE INDEX&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; delivered_orders&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ON&lt;&#x2F;span&gt;&lt;span&gt; orders(order_id) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE state =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;delivered&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;might seem beneficial, but in reality PostgreSQL will most likely chose to skip it and instead to scan the table. The reason? Should you consider the regular business operations, you might find vast majority (let&#x27;s say 95%) or orders shipped and considered finished. With such a high percentage of the records orders, the index will do little to narrow the search for the records. Planner hence will perform the sequential scan directly. Why? It&#x27;s all to do with the statistics that are available on the table.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;table_name        | orders&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;column_name       | state&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;n_distinct        | 4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;most_common_vals  | {delivered,cancelled,pending,open}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;most_common_freqs | {0.95023334,0.030166665,0.013,0.0096}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Those are fictional statistics matching the order distribution described above. From there you can see that &lt;code&gt;delivered&lt;&#x2F;code&gt; orders dominate the dataset, compromising approximately out of 95.02% of the records. The other statuses make up just for a small fraction.&lt;&#x2F;p&gt;
&lt;p&gt;The above data sample comes from the query similar to&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    tablename &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; table_name,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    attname &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;AS&lt;&#x2F;span&gt;&lt;span&gt; column_name,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    n_distinct,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    most_common_vals,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    most_common_freqs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pg_stats&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    tablename &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;orders&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    AND&lt;&#x2F;span&gt;&lt;span&gt; schemaname &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;public&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;outdated-statistics&quot;&gt;&lt;strong&gt;Outdated Statistics&lt;&#x2F;strong&gt;&lt;a class=&quot;zola-anchor&quot; href=&quot;#outdated-statistics&quot; aria-label=&quot;Anchor link for: outdated-statistics&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;As demonstrated low selectivity to addressed by statistics, instead of index, it&#x27;s not the only case where stale data can lead to inefficient query planning. Keeping database statistics current is crucial for PostgreSQL to make informed decisions about whether to use an index or opt for a sequential scan.&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL heavily relies on statistics to estimate costs and make the right decisions across different query execution plans. The statistics cover various data points, like total number of rows in table(s), distinct values, their distribution, histogram bounds, correlation between physical row ordering and column values, and much more.&lt;&#x2F;p&gt;
&lt;p&gt;The trouble starts when statistics get out-of-date. It&#x27;s similar as navigating using old maps. It just might you send you going in circles. How might the statistics in PostgreSQL get dated? Most cases involve &lt;strong&gt;high volume of data modifications&lt;&#x2F;strong&gt; or &lt;strong&gt;bulk operations&lt;&#x2F;strong&gt;, but will be affected also by schema changes. For all those operations it&#x27;s always good idea to &lt;code&gt;ANALYZE&lt;&#x2F;code&gt; the table to keep the DB up-to-date.&lt;&#x2F;p&gt;
&lt;p&gt;The prevent the statistics outdated, PostgreSQL alone either manual &lt;code&gt;ANALYZE&lt;&#x2F;code&gt; or periodically tries to trigger it as part of autovacuum daemon. The key factor is whatever it can run fast and frequently enough to keep up with the changes. You can adjust the autovaccuum settings globally or per-table.&lt;&#x2F;p&gt;
&lt;p&gt;Keeping statistics up-to-date is crucial for maintaining good query performance, as it ensures that the query planner has accurate information to base its decisions on.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;a class=&quot;zola-anchor&quot; href=&quot;#summary&quot; aria-label=&quot;Anchor link for: summary&quot;&gt;&lt;&#x2F;a&gt;
&lt;&#x2F;h2&gt;
&lt;p&gt;As demonstrated creating index might not be always the best course of action. Understanding those edge-cases is crucial not only for DBAs but also for developers. By keeping those considerations you can better design the schema and indexing strategy.&lt;&#x2F;p&gt;
&lt;p&gt;Indexes are still the next best thing in database world, but they require thoughtful implementation and maintenance. The goal is not only to create indexes, but to &lt;strong&gt;create the right indexes&lt;&#x2F;strong&gt; based on accurate, up-to-date data and aligned with your specific query patterns.&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
