<?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: Anusha Renangi</title>
    <description>The latest articles on DEV Community by Anusha Renangi (@anusha_renangi_05a4e41d35).</description>
    <link>https://dev.to/anusha_renangi_05a4e41d35</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%2F2788588%2Fc4db07f4-17e1-4e96-8de5-d8792aaf6428.jpeg</url>
      <title>DEV Community: Anusha Renangi</title>
      <link>https://dev.to/anusha_renangi_05a4e41d35</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anusha_renangi_05a4e41d35"/>
    <language>en</language>
    <item>
      <title>Database Indexing – Part 5: Sequential Scan vs. Index Scan vs. Bitmap Index Scan in PostgreSQL</title>
      <dc:creator>Anusha Renangi</dc:creator>
      <pubDate>Sun, 25 Oct 2026 14:23:19 +0000</pubDate>
      <link>https://dev.to/anusha_renangi_05a4e41d35/database-indexing-part-5-sequential-scan-vs-index-scan-vs-bitmap-index-scan-in-postgresql-e3m</link>
      <guid>https://dev.to/anusha_renangi_05a4e41d35/database-indexing-part-5-sequential-scan-vs-index-scan-vs-bitmap-index-scan-in-postgresql-e3m</guid>
      <description>&lt;p&gt;In my previous articles, we explored different types of indexes in PostgreSQL.&lt;/p&gt;

&lt;p&gt;But creating an index doesn't mean PostgreSQL will always use it.&lt;/p&gt;

&lt;p&gt;Depending on the query and the number of matching records, PostgreSQL may choose a Sequential Scan, an Index Scan, or a Bitmap Index Scan.&lt;/p&gt;

&lt;p&gt;In this article, we'll understand how these scan methods work using practical examples and PostgreSQL execution plans.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Scan Methods with an Example
&lt;/h2&gt;

&lt;p&gt;Let's use a &lt;code&gt;students&lt;/code&gt; table to understand when PostgreSQL chooses a Sequential Scan, Index Scan, or Bitmap Index Scan.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Creating the Table
&lt;/h3&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;TABLE&lt;/span&gt; &lt;span class="n"&gt;students&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;grades&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since &lt;code&gt;id&lt;/code&gt; is the primary key, PostgreSQL automatically creates an index on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Inserting Records
&lt;/h3&gt;

&lt;p&gt;Let's insert 1 million student records.&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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;students&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;grades&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="s1"&gt;'Student_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;grades&lt;/code&gt; column contains values from 1 to 100.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Creating an Index on Grades
&lt;/h3&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_students_grades&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;students&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grades&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we have two indexes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Primary key index:&lt;/strong&gt; On &lt;code&gt;id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate index:&lt;/strong&gt; On &lt;code&gt;grades&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Updating Table Statistics
&lt;/h3&gt;

&lt;p&gt;Before running our experiments, let's understand why updating table statistics is important.&lt;/p&gt;

&lt;p&gt;PostgreSQL uses statistics to estimate how many records a query might return. Based on these estimates, it decides whether scanning the table or using an index would be more efficient.&lt;/p&gt;

&lt;p&gt;We can check the statistics using:&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;relname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;n_live_tup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;n_mod_since_analyze&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_analyze&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_autoanalyze&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_user_tables&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'students'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;n_live_tup&lt;/code&gt;: Estimated number of live records.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;n_mod_since_analyze&lt;/code&gt;: Estimated number of records modified since the last analysis.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;last_analyze&lt;/code&gt;: Last time statistics were updated manually.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;last_autoanalyze&lt;/code&gt;: Last time statistics were updated automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Before ANALYZE:&lt;/strong&gt;&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%2Fwv8il51z1413q1jvpha4.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%2Fwv8il51z1413q1jvpha4.png" alt=" " width="800" height="144"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can see that &lt;code&gt;n_mod_since_analyze&lt;/code&gt; is &lt;strong&gt;1,000,000&lt;/strong&gt;, meaning PostgreSQL has recorded approximately 1 million modifications since the last analysis.&lt;/p&gt;

&lt;p&gt;Now, let's update the statistics manually.&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="n"&gt;students&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After ANALYZE:&lt;/strong&gt;&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%2F6cd01zs0ykhzs4n3ckgi.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%2F6cd01zs0ykhzs4n3ckgi.png" alt=" " width="800" height="144"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After running &lt;code&gt;ANALYZE&lt;/code&gt;, we can observe that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;n_mod_since_analyze&lt;/code&gt; changed from &lt;strong&gt;1,000,000 to 0&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;last_analyze&lt;/code&gt; was updated with the execution time.&lt;/li&gt;
&lt;li&gt;PostgreSQL refreshed the statistics used for estimating query costs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostgreSQL also performs this automatically through &lt;strong&gt;auto-analyze&lt;/strong&gt;. We ran it manually here to understand how the statistics change.&lt;/p&gt;

&lt;p&gt;Refer more &lt;a href="https://www.postgresql.org/docs/17/runtime-config-autovacuum.html?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now that our table and indexes are ready, let's run a few queries and observe which scan methods PostgreSQL chooses.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Index Scan
&lt;/h2&gt;

&lt;p&gt;Let's start with a query that retrieves a small number of records from our &lt;code&gt;students&lt;/code&gt; 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;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="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;students&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Plan:&lt;/strong&gt;&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%2Flbpqs44lio710ltwcwm3.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%2Flbpqs44lio710ltwcwm3.png" alt=" " width="798" height="163"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL chose an &lt;strong&gt;Index Scan&lt;/strong&gt; using &lt;code&gt;students_pkey&lt;/code&gt;, the primary key index on &lt;code&gt;id&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Does an Index Scan Work?
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;PostgreSQL searches the index to find entries where &lt;code&gt;id &amp;lt; 100&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Each matching index entry contains a reference to the corresponding row in the table (heap).&lt;/li&gt;
&lt;li&gt;PostgreSQL uses these references to fetch the rows from the heap and retrieve the &lt;code&gt;name&lt;/code&gt; values.&lt;/li&gt;
&lt;li&gt;This process continues until all matching records are retrieved.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Since we're selecting &lt;code&gt;name&lt;/code&gt;, which is not stored in the primary key index, PostgreSQL needs to visit the heap to retrieve it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Did PostgreSQL Choose an Index Scan?
&lt;/h3&gt;

&lt;p&gt;Our table contains 1 million records, but this query needs only 99.&lt;/p&gt;

&lt;p&gt;Instead of scanning the entire table, PostgreSQL uses the primary key index to locate the matching records.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;Index Scan is generally useful when a query retrieves a small number of records&lt;/strong&gt;, especially when the required rows can be found through an index without scanning most of the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Sequential Scan
&lt;/h2&gt;

&lt;p&gt;Now, let's change the condition to retrieve most of the records from our &lt;code&gt;students&lt;/code&gt; 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;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="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;students&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Plan:&lt;/strong&gt;&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%2Fhl1saebqv539yq8bxois.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%2Fhl1saebqv539yq8bxois.png" alt=" " width="800" height="171"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This time, PostgreSQL chose a &lt;strong&gt;Sequential Scan (Seq Scan)&lt;/strong&gt;, even though we already have an index on &lt;code&gt;id&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Does a Sequential Scan Work?
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;PostgreSQL reads the table pages sequentially.&lt;/li&gt;
&lt;li&gt;It checks each row against the condition &lt;code&gt;id &amp;gt; 100&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If the row matches, PostgreSQL retrieves its &lt;code&gt;name&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;This continues until the entire table has been scanned.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why Didn't PostgreSQL Use the Index?
&lt;/h3&gt;

&lt;p&gt;Our table contains 1 million records, and this query returns almost all of them.&lt;/p&gt;

&lt;p&gt;If PostgreSQL used an Index Scan, it would need to find the matching entries in the index and then visit the corresponding heap rows to retrieve their names.&lt;/p&gt;

&lt;p&gt;Since almost every row matches, repeatedly accessing the table through index entries would involve unnecessary work.&lt;/p&gt;

&lt;p&gt;Instead, PostgreSQL chose to scan the table directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Sequential Scan can be more efficient than an Index Scan when a query needs to retrieve a large portion of the table.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Bitmap Index Scan
&lt;/h2&gt;

&lt;p&gt;Now, let's filter students whose grades are greater than 95.&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="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;students&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;grades&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Plan:&lt;/strong&gt;&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%2F2opjtcrgcwb6le5tarhv.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%2F2opjtcrgcwb6le5tarhv.png" alt=" " width="800" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL used a &lt;strong&gt;Bitmap Index Scan&lt;/strong&gt; on &lt;code&gt;idx_students_grades&lt;/code&gt;, followed by a &lt;strong&gt;Bitmap Heap Scan&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Does a Bitmap Index Scan Work?
&lt;/h3&gt;

&lt;p&gt;In a regular Index Scan, PostgreSQL uses the index to locate matching rows and fetches them from the heap.&lt;/p&gt;

&lt;p&gt;In a Bitmap Index Scan, PostgreSQL first collects the locations of all matching rows into a bitmap. The Bitmap Heap Scan then uses this information to fetch the required heap pages.&lt;/p&gt;

&lt;p&gt;Let's take a small example.&lt;/p&gt;

&lt;p&gt;Each index entry contains a &lt;strong&gt;TID (Tuple Identifier)&lt;/strong&gt;, which identifies a row using its heap page number and tuple offset.&lt;/p&gt;

&lt;p&gt;Suppose the index finds these matching locations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Page 10 → Tuples [2, 5, 8]
Page 20 → Tuples [3, 7]
Page 30 → Tuple [4]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL stores these locations in a bitmap and processes the heap pages in page order.&lt;/p&gt;

&lt;p&gt;When Page 10 is fetched into memory, the entire page (usually 8 KB) is loaded, including all the tuples stored on it.&lt;/p&gt;

&lt;p&gt;However, PostgreSQL already knows that only tuples 2, 5, and 8 matched the index condition. So it directly accesses those tuples, checks their visibility, and retrieves the required values.&lt;/p&gt;

&lt;p&gt;This is called an &lt;strong&gt;exact bitmap&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;PostgreSQL can also combine multiple indexes using &lt;strong&gt;BitmapAnd&lt;/strong&gt; and &lt;strong&gt;BitmapOr&lt;/strong&gt; when queries have multiple filtering conditions.&lt;/p&gt;

&lt;p&gt;I've explained both methods with practical examples in my previous article: &lt;a href="https://dev.to/anusha_renangi_05a4e41d35/database-indexing-part-3-understanding-composite-indexes-in-postgresql-2g5"&gt;Database Indexing – Part 3: Understanding Composite Indexes in PostgreSQL&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Did PostgreSQL Choose a Bitmap Index Scan?
&lt;/h3&gt;

&lt;p&gt;Our table contains 1 million records, and this query returns 50,000 records.&lt;/p&gt;

&lt;p&gt;Using a regular Index Scan could involve repeatedly accessing heap pages for individual matching rows.&lt;/p&gt;

&lt;p&gt;A Sequential Scan would need to scan the entire table.&lt;/p&gt;

&lt;p&gt;A Bitmap Index Scan allows PostgreSQL to collect the matching row locations first and then access the required heap pages in page order, reducing unnecessary page accesses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding Recheck Cond and Lossy Bitmap Pages
&lt;/h3&gt;

&lt;p&gt;If we look at our execution plan, we can see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Recheck Cond: (grades &amp;gt; 95)
Heap Blocks: exact=7352
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;exact=7352&lt;/code&gt; means PostgreSQL had the exact matching tuple locations for all 7,352 heap pages.&lt;/p&gt;

&lt;p&gt;But storing all these tuple locations requires memory.&lt;/p&gt;

&lt;p&gt;When there isn't enough memory, PostgreSQL can convert some exact bitmap entries into &lt;strong&gt;lossy entries&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of remembering the matching tuple offsets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Page 10 → Tuples [2, 5, 8]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It remembers only the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Page 10 → May contain matching tuples
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When PostgreSQL fetches this page, it has to examine the candidate tuples and check the condition &lt;code&gt;grades &amp;gt; 95&lt;/code&gt; again, since the bitmap no longer contains the exact matching offsets.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;&lt;code&gt;Recheck Cond&lt;/code&gt;&lt;/strong&gt; is used.&lt;/p&gt;

&lt;p&gt;Let's try this practically by reducing &lt;code&gt;work_mem&lt;/code&gt; to 64 KB.&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;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;LOCAL&lt;/span&gt; &lt;span class="n"&gt;work_mem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'64kB'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&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="n"&gt;name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;students&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;grades&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;ROLLBACK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Plan:&lt;/strong&gt;&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%2Fb6en8yijur8p2vmfd2cf.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%2Fb6en8yijur8p2vmfd2cf.png" alt=" " width="799" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This time, PostgreSQL used a Parallel Bitmap Heap Scan.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Recheck Cond: (grades &amp;gt; 95)
Rows Removed by Index Recheck: 288507
Heap Blocks: exact=209 lossy=1660
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;exact=209&lt;/code&gt;:&lt;/strong&gt; Heap-block accesses with exact tuple-location information.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;lossy=1660&lt;/code&gt;:&lt;/strong&gt; Heap-block accesses where only page-level information was available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Rows Removed by Index Recheck: 288507&lt;/code&gt;:&lt;/strong&gt; Candidate rows that PostgreSQL rejected after evaluating the condition again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since this was a parallel scan, the heap-block counts shown are not necessarily the total number of distinct pages processed by all workers.&lt;/p&gt;

&lt;p&gt;With sufficient memory, PostgreSQL can remember the exact matching tuple locations. When memory is limited, it may retain only page locations and recheck the filtering condition while scanning the heap.&lt;/p&gt;

&lt;p&gt;PostgreSQL doesn't always use an index just because one is available. It chooses a scan method based on what it estimates will be most efficient for the query.&lt;/p&gt;

&lt;p&gt;In the next article, we'll explore how PostgreSQL's query optimizer makes these decisions.&lt;/p&gt;

&lt;p&gt;Thanks for reading! I hope this article helped you understand how different scan methods work in PostgreSQL.&lt;/p&gt;

</description>
      <category>database</category>
      <category>performance</category>
      <category>postgres</category>
      <category>sql</category>
    </item>
    <item>
      <title>Database Indexing – Part 4: Covering vs. Composite Indexes in PostgreSQL – Which One Should You Use?</title>
      <dc:creator>Anusha Renangi</dc:creator>
      <pubDate>Fri, 23 Oct 2026 15:29:47 +0000</pubDate>
      <link>https://dev.to/anusha_renangi_05a4e41d35/database-indexing-part-4-covering-vs-composite-indexes-in-postgresql-which-one-should-you-use-5786</link>
      <guid>https://dev.to/anusha_renangi_05a4e41d35/database-indexing-part-4-covering-vs-composite-indexes-in-postgresql-which-one-should-you-use-5786</guid>
      <description>&lt;p&gt;In my previous articles, we explored &lt;a href="https://dev.to/anusha_renangi_05a4e41d35/database-indexing-part-2-understanding-covering-indexes-in-postgresql-17ee"&gt;covering indexes&lt;/a&gt; and &lt;a href="https://dev.to/anusha_renangi_05a4e41d35/database-indexing-part-3-understanding-composite-indexes-in-postgresql-2g5"&gt;composite indexes&lt;/a&gt; separately.&lt;/p&gt;

&lt;p&gt;Both indexes can improve query performance, but how do they differ in terms of storage, INSERT, UPDATE, and SELECT performance?&lt;/p&gt;

&lt;p&gt;To understand this, I tested both index types in PostgreSQL using two different datasets: one with unique values in the first indexed column and another with duplicate values.&lt;/p&gt;

&lt;p&gt;In this article, we'll compare the results of these experiments and understand when to use covering and composite indexes in real-world applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment 1: Covering vs. Composite Indexes with Unique Key Values
&lt;/h2&gt;

&lt;p&gt;To compare covering and composite indexes, we'll perform a few experiments in PostgreSQL using &lt;strong&gt;10 million records&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We'll use two separate tables with identical structures and data, so we can test both index types independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Creating the Tables
&lt;/h3&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;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents_covering&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;document_id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;document_key&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;document_version&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents_composite&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;document_id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;document_key&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;document_version&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&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;h3&gt;
  
  
  2. Inserting 10 Million Records
&lt;/h3&gt;

&lt;p&gt;First, let's populate the covering-index 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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;documents_covering&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="s1"&gt;'DOC_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'VERSION_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&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;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10000000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, copy the same records into the composite-index 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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;documents_composite&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents_covering&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Creating the Indexes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Covering Index:&lt;/strong&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_documents_covering&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents_covering&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Composite Index:&lt;/strong&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_documents_composite&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents_composite&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, update the table statistics.&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="n"&gt;documents_covering&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ANALYZE&lt;/span&gt; &lt;span class="n"&gt;documents_composite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both tables now contain the same 10 million records, with different index configurations.&lt;/p&gt;

&lt;p&gt;We'll compare their &lt;strong&gt;storage usage, SELECT, INSERT, and UPDATE performance&lt;/strong&gt; to understand how these indexes behave.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Storage Comparison
&lt;/h2&gt;

&lt;p&gt;Let's start by comparing how much storage each index occupies after indexing 10 million records.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 1: Comparing Index Sizes
&lt;/h3&gt;

&lt;p&gt;We can use PostgreSQL's &lt;code&gt;pg_relation_size()&lt;/code&gt; function to check the storage occupied by each 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;SELECT&lt;/span&gt;
    &lt;span class="s1"&gt;'Covering Index'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;index_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'idx_documents_covering'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;index_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'idx_documents_covering'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;size_bytes&lt;/span&gt;
&lt;span class="k"&gt;UNION&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="s1"&gt;'Composite Index'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'idx_documents_composite'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'idx_documents_composite'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&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%2Fua948xcwex88gywpyp6q.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%2Fua948xcwex88gywpyp6q.png" alt=" " width="798" height="161"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both indexes occupied exactly the same storage space: &lt;strong&gt;3,625 MB (3,801,243,648 bytes)&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 2: Comparing Index Pages
&lt;/h3&gt;

&lt;p&gt;Next, let's check the number of pages occupied by each index using PostgreSQL's &lt;code&gt;pg_class&lt;/code&gt; system catalog.&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;relname&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;index_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;relpages&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_pages&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_class&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;relname&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'idx_documents_covering'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'idx_documents_composite'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&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%2F5ecalgmkllju3j10g1nl.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%2F5ecalgmkllju3j10g1nl.png" alt=" " width="798" height="161"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Are Their Sizes the Same?
&lt;/h3&gt;

&lt;p&gt;PostgreSQL uses a B-tree structure that works similarly to a &lt;strong&gt;B+ tree&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In PostgreSQL, B-tree indexes contain two main types of pages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Leaf pages:&lt;/strong&gt; Store the actual index entries containing indexed values and references to table rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal pages:&lt;/strong&gt; Store separator keys and pointers that help PostgreSQL navigate to the correct leaf pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can read more about this in the &lt;a href="https://www.postgresql.org/docs/16/btree-implementation.html#BTREE-STRUCTURE" rel="noopener noreferrer"&gt;PostgreSQL B-tree Implementation documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;PostgreSQL also mentions that typically &lt;strong&gt;more than 99% of B-tree pages are leaf pages&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Both covering and composite indexes store their indexed column values in leaf entries. The difference is in how PostgreSQL handles those columns in internal pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Covering Index&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a covering index, additional columns are included using &lt;code&gt;INCLUDE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These columns are stored in leaf entries for data retrieval, but they are not needed for searching or navigating the B-tree.&lt;/p&gt;

&lt;p&gt;So, PostgreSQL completely excludes included columns from internal navigation entries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composite Index&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a composite index, all columns are key columns and can participate in searching and sorting.&lt;/p&gt;

&lt;p&gt;However, PostgreSQL has an optimization called &lt;strong&gt;suffix truncation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Suffix truncation removes unnecessary trailing key columns from internal B-tree entries.&lt;/p&gt;

&lt;p&gt;For example, if the first indexed column is enough to distinguish two page ranges, PostgreSQL doesn't need to store the second column in the corresponding internal separator.&lt;/p&gt;

&lt;p&gt;You can read about this in the &lt;a href="https://www.postgresql.org/docs/16/indexes-index-only-scans.html" rel="noopener noreferrer"&gt;PostgreSQL documentation on suffix truncation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In our case, &lt;code&gt;document_key&lt;/code&gt; contains unique values, so the second column is generally unnecessary for distinguishing page ranges.&lt;/p&gt;

&lt;p&gt;Since both indexes store the same column values in their leaf entries, and their internal entries can also be similarly compact, their storage requirements can be very close.&lt;/p&gt;

&lt;p&gt;In our experiment, both indexes occupied exactly &lt;strong&gt;3,625 MB across 464,019 pages&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. SELECT Performance Comparison
&lt;/h2&gt;

&lt;p&gt;Next, let's compare how covering and composite indexes perform when retrieving records.&lt;/p&gt;

&lt;p&gt;We'll select 6,500 records using &lt;code&gt;document_key&lt;/code&gt; and retrieve both &lt;code&gt;document_key&lt;/code&gt; and &lt;code&gt;document_version&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We'll execute each query twice to compare the initial run and the warm-cache run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 1: Covering Index
&lt;/h3&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="n"&gt;document_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents_covering&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;document_key&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt;
    &lt;span class="s1"&gt;'DOC_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'100000'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt;
    &lt;span class="s1"&gt;'DOC_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'106499'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Initial Run:&lt;/strong&gt;&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%2Fb455by6vlp3z6zhysk8u.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%2Fb455by6vlp3z6zhysk8u.png" alt=" " width="798" height="161"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warm-Cache Run:&lt;/strong&gt;&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%2F6yndtpufun6e8o3b0rlo.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%2F6yndtpufun6e8o3b0rlo.png" alt=" " width="798" height="161"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 2: Composite Index
&lt;/h3&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="n"&gt;document_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents_composite&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;document_key&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt;
    &lt;span class="s1"&gt;'DOC_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'100000'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;AND&lt;/span&gt;
    &lt;span class="s1"&gt;'DOC_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'106499'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Initial Run:&lt;/strong&gt;&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%2Fy8fvafobj40uvjd7zo1b.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%2Fy8fvafobj40uvjd7zo1b.png" alt=" " width="798" height="161"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Warm-Cache Run:&lt;/strong&gt;&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%2F97eh1px8lj8106qlgm2c.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%2F97eh1px8lj8106qlgm2c.png" alt=" " width="798" height="161"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Results Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Covering Index&lt;/th&gt;
&lt;th&gt;Composite Index&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Initial execution time&lt;/td&gt;
&lt;td&gt;25.255 ms&lt;/td&gt;
&lt;td&gt;29.351 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm-cache execution time&lt;/td&gt;
&lt;td&gt;26.493 ms&lt;/td&gt;
&lt;td&gt;24.660 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rows returned&lt;/td&gt;
&lt;td&gt;6,500&lt;/td&gt;
&lt;td&gt;6,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Initial shared-buffer reads&lt;/td&gt;
&lt;td&gt;301&lt;/td&gt;
&lt;td&gt;301&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm-cache shared-buffer hits&lt;/td&gt;
&lt;td&gt;301&lt;/td&gt;
&lt;td&gt;301&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heap fetches&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Observations
&lt;/h3&gt;

&lt;p&gt;Both indexes used an &lt;strong&gt;Index Only Scan&lt;/strong&gt;, meaning PostgreSQL retrieved the required column values directly from the indexes.&lt;/p&gt;

&lt;p&gt;Both also reported &lt;strong&gt;0 heap fetches&lt;/strong&gt;, so PostgreSQL didn't need to visit the table rows to check their visibility.&lt;/p&gt;

&lt;p&gt;During the initial runs, both indexes reported 301 shared-buffer reads. During the warm-cache runs, those became 301 shared-buffer hits, as the required index pages were already available in PostgreSQL's shared buffers.&lt;/p&gt;

&lt;p&gt;The execution times were nearly identical, with small variations between runs.&lt;/p&gt;

&lt;p&gt;For this SELECT query, &lt;strong&gt;neither index showed a clear performance advantage&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. INSERT Performance Comparison
&lt;/h2&gt;

&lt;p&gt;Next, let's compare INSERT performance between covering and composite indexes.&lt;/p&gt;

&lt;p&gt;Both tables already contain 10 million records. We'll insert another &lt;strong&gt;100,000 records&lt;/strong&gt; into each table and measure the execution time using &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS)&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 1: Covering Index
&lt;/h3&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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;documents_covering&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&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;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'DOC_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'VERSION_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&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;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10000001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10100000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Result:&lt;/strong&gt;&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%2F7s4behbq45gtxzhuzkzx.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%2F7s4behbq45gtxzhuzkzx.png" alt=" " width="798" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Execution time: &lt;strong&gt;4,814.892 ms&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 2: Composite Index
&lt;/h3&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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;documents_composite&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&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;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'DOC_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'VERSION_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&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;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10000001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10100000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Result:&lt;/strong&gt;&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%2Fe9q18hijzo9hkirl1hnq.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%2Fe9q18hijzo9hkirl1hnq.png" alt=" " width="798" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Execution time: &lt;strong&gt;4,681.164 ms&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Results Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Index Type&lt;/th&gt;
&lt;th&gt;Records Inserted&lt;/th&gt;
&lt;th&gt;Execution Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Covering Index&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;4,814.892 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composite Index&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;4,681.164 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Observations
&lt;/h3&gt;

&lt;p&gt;Both indexes showed similar INSERT performance, with the composite index completing around &lt;strong&gt;134 ms faster&lt;/strong&gt; in this run.&lt;/p&gt;

&lt;p&gt;When inserting records, PostgreSQL needs to write the new rows to the table and create the corresponding entries in its indexes.&lt;/p&gt;

&lt;p&gt;Both index types store the same column values in their leaf entries, so the amount of index maintenance can be similar.&lt;/p&gt;

&lt;p&gt;These tests were performed after deleting and vacuuming previously inserted records, so existing free space and index maintenance may also affect the results.&lt;/p&gt;

&lt;p&gt;We didn't observe a significant performance difference between covering and composite indexes in this INSERT test.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. UPDATE Performance Comparison
&lt;/h2&gt;

&lt;p&gt;Next, let's compare how covering and composite indexes handle updates.&lt;/p&gt;

&lt;p&gt;We'll update &lt;code&gt;document_version&lt;/code&gt; for &lt;strong&gt;100,000 existing records&lt;/strong&gt; in each table.&lt;/p&gt;

&lt;p&gt;In the covering index, &lt;code&gt;document_version&lt;/code&gt; is an included column, while in the composite index, it's a key column.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 1: Covering Index
&lt;/h3&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;documents_covering&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'UPDATED_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;190&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&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;document_id&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Result:&lt;/strong&gt;&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%2Fru1xh4mz2wkpmn6z8m98.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%2Fru1xh4mz2wkpmn6z8m98.png" alt=" " width="798" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Execution time: &lt;strong&gt;5,165.132 ms&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 2: Composite Index
&lt;/h3&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;documents_composite&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'UPDATED_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;190&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&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;document_id&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Result:&lt;/strong&gt;&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%2Fs6jacf2maum5fvba38nk.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%2Fs6jacf2maum5fvba38nk.png" alt=" " width="798" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Execution time: &lt;strong&gt;5,691.076 ms&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Results Comparison
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Index Type&lt;/th&gt;
&lt;th&gt;Records Updated&lt;/th&gt;
&lt;th&gt;Execution Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Covering Index&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;5,165.132 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composite Index&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;5,691.076 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Observations
&lt;/h3&gt;

&lt;p&gt;Both indexes showed relatively similar UPDATE performance, with the covering index completing around &lt;strong&gt;526 ms faster&lt;/strong&gt; in this test.&lt;/p&gt;

&lt;p&gt;When an indexed column is updated, PostgreSQL needs to maintain the corresponding index entries.&lt;/p&gt;

&lt;p&gt;This applies to both index types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Covering Index:&lt;/strong&gt; Updating an included column requires index maintenance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composite Index:&lt;/strong&gt; Updating a key column also requires index maintenance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Normally, PostgreSQL can sometimes perform a &lt;strong&gt;HOT (Heap-Only Tuple) update&lt;/strong&gt;, avoiding the need to create new index entries.&lt;/p&gt;

&lt;p&gt;However, updating a column referenced by a regular B-tree index prevents HOT updates, whether the column is a key or an included column. You can read about this in the &lt;a href="https://www.postgresql.org/docs/current/storage-hot.html" rel="noopener noreferrer"&gt;PostgreSQL HOT Updates documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In our experiment, both indexes required index maintenance, and neither showed a clear, consistent performance advantage based on this single test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment 2: Covering vs. Composite Indexes with Duplicate Key Values
&lt;/h2&gt;

&lt;p&gt;In our first experiment, &lt;code&gt;document_key&lt;/code&gt; contained unique values, and both indexes showed almost identical storage usage and similar performance.&lt;/p&gt;

&lt;p&gt;Now, let's see what happens when the first indexed column contains duplicate values.&lt;/p&gt;

&lt;p&gt;We'll create two new tables with &lt;strong&gt;1 million records&lt;/strong&gt;, where each &lt;code&gt;document_key&lt;/code&gt; repeats 10,000 times, while &lt;code&gt;document_version&lt;/code&gt; remains unique.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up the Experiment
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Creating the Tables&lt;/strong&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents_duplicate_covering&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;document_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;document_key&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;document_version&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents_duplicate_composite&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;document_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;document_key&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;document_version&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&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;&lt;strong&gt;2. Inserting 1 Million Records&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First, insert the records into the covering-index 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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;documents_duplicate_covering&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&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;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'DOC_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'VERSION_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&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;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy the same data into the composite-index 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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;documents_duplicate_composite&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents_duplicate_covering&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Creating the Indexes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Covering 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_duplicate_covering&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents_duplicate_covering&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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_duplicate_composite&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents_duplicate_composite&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both tables now contain identical data, but their indexes handle &lt;code&gt;document_version&lt;/code&gt; differently.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Storage Comparison
&lt;/h3&gt;

&lt;p&gt;Let's first compare the index sizes after creating both indexes on 1 million records.&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;indexrelid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;index_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;index_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pg_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indexrelid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;size_bytes&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_index&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;indexrelid&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'idx_duplicate_covering'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'idx_duplicate_composite'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&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%2F90a0mf7ji67ckz3x3lgl.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%2F90a0mf7ji67ckz3x3lgl.png" alt=" " width="800" height="164"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Index Type&lt;/th&gt;
&lt;th&gt;Index Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Covering Index&lt;/td&gt;
&lt;td&gt;363 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composite Index&lt;/td&gt;
&lt;td&gt;376 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This time, the composite index occupied approximately &lt;strong&gt;13 MB more storage&lt;/strong&gt; than the covering index.&lt;/p&gt;

&lt;p&gt;Unlike our first experiment, the leading indexed column now contains duplicate values.&lt;/p&gt;

&lt;p&gt;In a covering index, PostgreSQL stores included columns only in leaf entries.&lt;/p&gt;

&lt;p&gt;In a composite index, trailing key columns may also be needed in internal B-tree entries to distinguish page ranges when the leading column contains duplicate values.&lt;/p&gt;

&lt;p&gt;Both indexes still store the same column values in their leaf entries, but their internal navigation entries can differ.&lt;/p&gt;

&lt;p&gt;In our experiment, the composite index required slightly more storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. SELECT Performance Comparison
&lt;/h3&gt;

&lt;p&gt;Next, let's retrieve records using a &lt;code&gt;document_key&lt;/code&gt; that appears multiple times.&lt;/p&gt;

&lt;p&gt;Before running the queries, we updated the table statistics and visibility information.&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="n"&gt;documents_duplicate_covering&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&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="n"&gt;documents_duplicate_composite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Query 1: Covering Index&lt;/strong&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;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="n"&gt;document_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents_duplicate_covering&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;document_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'DOC_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'50'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&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%2Fk655fnm1yvs9owm4xitv.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%2Fk655fnm1yvs9owm4xitv.png" alt=" " width="798" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query 2: Composite Index&lt;/strong&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;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="n"&gt;document_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents_duplicate_composite&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;document_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'DOC_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'50'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&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%2F1a10g45a5tm7lymjkozw.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%2F1a10g45a5tm7lymjkozw.png" alt=" " width="798" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Covering Index&lt;/th&gt;
&lt;th&gt;Composite Index&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Records returned&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution time&lt;/td&gt;
&lt;td&gt;2.846 ms&lt;/td&gt;
&lt;td&gt;2.626 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scan type&lt;/td&gt;
&lt;td&gt;Index Only Scan&lt;/td&gt;
&lt;td&gt;Index Only Scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heap fetches&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared-buffer reads&lt;/td&gt;
&lt;td&gt;458&lt;/td&gt;
&lt;td&gt;459&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Observations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both indexes used an &lt;strong&gt;Index Only Scan&lt;/strong&gt;, returning 10,000 records without fetching any rows from the table heap.&lt;/p&gt;

&lt;p&gt;The execution times were almost identical, and both accessed approximately the same number of index pages.&lt;/p&gt;

&lt;p&gt;Even with duplicate values in the first indexed column, we didn't observe a noticeable SELECT performance difference.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. INSERT Performance Comparison
&lt;/h3&gt;

&lt;p&gt;Now, let's insert another &lt;strong&gt;100,000 records&lt;/strong&gt; into each table while keeping duplicate values in &lt;code&gt;document_key&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query 1: Covering Index&lt;/strong&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;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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;documents_duplicate_covering&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&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;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'DOC_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'VERSION_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&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;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1100000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&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%2Fgvvy0l714x18k4uprc0u.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%2Fgvvy0l714x18k4uprc0u.png" alt=" " width="799" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query 2: Composite Index&lt;/strong&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;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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;documents_duplicate_composite&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&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;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'DOC_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'VERSION_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;192&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&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;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1100000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&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%2Fh2a5b46hv1r5uo0hffr3.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%2Fh2a5b46hv1r5uo0hffr3.png" alt=" " width="799" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Covering Index&lt;/th&gt;
&lt;th&gt;Composite Index&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Records inserted&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution time&lt;/td&gt;
&lt;td&gt;2,892.511 ms&lt;/td&gt;
&lt;td&gt;5,710.005 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Observations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This time, the covering index completed the INSERT in approximately &lt;strong&gt;2.89 seconds&lt;/strong&gt;, while the composite index took &lt;strong&gt;5.71 seconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The composite-index test also performed more buffer reads and writes.&lt;/p&gt;

&lt;p&gt;The difference was much larger than in our first experiment. However, these are single-run measurements, and cache conditions and disk activity can also affect the execution time.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. UPDATE Performance Comparison
&lt;/h3&gt;

&lt;p&gt;Finally, let's update &lt;code&gt;document_version&lt;/code&gt; for &lt;strong&gt;100,000 records&lt;/strong&gt; in both tables.&lt;/p&gt;

&lt;p&gt;Before the test, we ran:&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="n"&gt;documents_duplicate_covering&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&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="n"&gt;documents_duplicate_composite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Query 1: Covering Index&lt;/strong&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;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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;documents_duplicate_covering&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'UPDATED_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;190&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&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;document_id&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&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%2Fonu7z9d4mdoqh5u7vlgl.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%2Fonu7z9d4mdoqh5u7vlgl.png" alt=" " width="799" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query 2: Composite Index&lt;/strong&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;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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;documents_duplicate_composite&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'UPDATED_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;LPAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;190&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'0'&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;document_id&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&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%2Fojj6paw6bp6n975ije21.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%2Fojj6paw6bp6n975ije21.png" alt=" " width="799" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Covering Index&lt;/th&gt;
&lt;th&gt;Composite Index&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Records updated&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution time&lt;/td&gt;
&lt;td&gt;3,288.368 ms&lt;/td&gt;
&lt;td&gt;4,598.804 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scan type&lt;/td&gt;
&lt;td&gt;Sequential Scan&lt;/td&gt;
&lt;td&gt;Sequential Scan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Observations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The covering index completed the UPDATE approximately &lt;strong&gt;1.31 seconds faster&lt;/strong&gt; than the composite index in this test.&lt;/p&gt;

&lt;p&gt;Both queries used a Sequential Scan because these new tables didn't have an index on &lt;code&gt;document_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Also, changing &lt;code&gt;document_version&lt;/code&gt; required index maintenance in both cases, whether it was an included column or a composite-index key column.&lt;/p&gt;

&lt;p&gt;The composite-index test recorded more buffer writes, which may have contributed to its longer execution time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparison of Both Experiments
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Experiment 1: Unique Values in the First Key Column
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Covering Index&lt;/th&gt;
&lt;th&gt;Composite Index&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Storage (10 million records)&lt;/td&gt;
&lt;td&gt;3,625 MB&lt;/td&gt;
&lt;td&gt;3,625 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SELECT (6,500 records, warm cache)&lt;/td&gt;
&lt;td&gt;26.493 ms&lt;/td&gt;
&lt;td&gt;24.660 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INSERT (100,000 records)&lt;/td&gt;
&lt;td&gt;4,814.892 ms&lt;/td&gt;
&lt;td&gt;4,681.164 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPDATE (100,000 records)&lt;/td&gt;
&lt;td&gt;5,165.132 ms&lt;/td&gt;
&lt;td&gt;5,691.076 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Experiment 2: Duplicate Values in the First Key Column
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Covering Index&lt;/th&gt;
&lt;th&gt;Composite Index&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Storage (1 million records)&lt;/td&gt;
&lt;td&gt;363 MB&lt;/td&gt;
&lt;td&gt;376 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SELECT (10,000 records)&lt;/td&gt;
&lt;td&gt;2.846 ms&lt;/td&gt;
&lt;td&gt;2.626 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INSERT (100,000 records)&lt;/td&gt;
&lt;td&gt;2,892.511 ms&lt;/td&gt;
&lt;td&gt;5,710.005 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPDATE (100,000 records)&lt;/td&gt;
&lt;td&gt;3,288.368 ms&lt;/td&gt;
&lt;td&gt;4,598.804 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In the first experiment, both indexes occupied the same storage space and showed similar SELECT, INSERT, and UPDATE performance.&lt;/p&gt;

&lt;p&gt;In the second experiment, where the first indexed column contained duplicate values, the composite index occupied more storage. The covering index also completed the INSERT and UPDATE operations faster in our tests.&lt;/p&gt;

&lt;p&gt;This shows why we should carefully decide which columns need to be index keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which One Should You Choose: Composite Index or Covering Index?
&lt;/h2&gt;

&lt;p&gt;In real-world applications, covering and composite indexes are not competing approaches. We can combine both in a single index.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Key columns:&lt;/strong&gt; Keep columns required for filtering, joining, or sorting as index keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-key columns:&lt;/strong&gt; Keep additional columns needed only for retrieving data in the &lt;code&gt;INCLUDE&lt;/code&gt; clause.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unnecessarily adding columns as index keys can waste storage, especially when the leading columns contain duplicate values.&lt;/p&gt;

&lt;p&gt;PostgreSQL may need those additional key columns in internal B-tree entries. But columns added using &lt;code&gt;INCLUDE&lt;/code&gt; are stored only in leaf entries, which can help reduce unnecessary storage.&lt;/p&gt;

&lt;p&gt;At the same time, we shouldn't add too many columns to &lt;code&gt;INCLUDE&lt;/code&gt;, because they still increase index size and require maintenance whenever their values change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So, keep only the required columns as index keys, and use &lt;code&gt;INCLUDE&lt;/code&gt; for additional columns needed only for retrieval. Choose both carefully based on your actual query requirements.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading! I hope this article helped you better understand covering and composite indexes in PostgreSQL.&lt;/p&gt;

</description>
      <category>database</category>
      <category>performance</category>
      <category>postgres</category>
      <category>sql</category>
    </item>
    <item>
      <title>Database Indexing – Part 3: Understanding Composite Indexes in PostgreSQL</title>
      <dc:creator>Anusha Renangi</dc:creator>
      <pubDate>Wed, 21 Oct 2026 18:40:47 +0000</pubDate>
      <link>https://dev.to/anusha_renangi_05a4e41d35/database-indexing-part-3-understanding-composite-indexes-in-postgresql-2g5</link>
      <guid>https://dev.to/anusha_renangi_05a4e41d35/database-indexing-part-3-understanding-composite-indexes-in-postgresql-2g5</guid>
      <description>&lt;p&gt;In PostgreSQL, we often come across queries that filter records based on multiple conditions.&lt;/p&gt;

&lt;p&gt;Consider an &lt;code&gt;employees&lt;/code&gt; table containing information such as employee ID, name, department, and salary.&lt;/p&gt;

&lt;p&gt;Suppose we want to find employees working in the Engineering department who earn more than 50,000.&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="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Engineering'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query looks simple, but what happens when the table contains millions of employee records?&lt;/p&gt;

&lt;p&gt;PostgreSQL needs to identify the rows that satisfy both conditions. As the amount of data grows, finding those records efficiently becomes more important.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How can we optimize queries that filter on multiple columns?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this article, we'll explore composite indexes in PostgreSQL and understand how they can help with such queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is a Composite Index?
&lt;/h2&gt;

&lt;p&gt;A composite index, also called a &lt;strong&gt;multicolumn index&lt;/strong&gt;, is an index created on two or more columns of a table.&lt;/p&gt;

&lt;p&gt;In PostgreSQL, we can create a composite index using:&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_employees_department_salary&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;department&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we're creating a single index on two columns: &lt;code&gt;department&lt;/code&gt; and &lt;code&gt;salary&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Unlike separate indexes on each column, a composite index stores the values together in a specific order.&lt;/p&gt;

&lt;p&gt;PostgreSQL organizes the index entries &lt;strong&gt;first by &lt;code&gt;department&lt;/code&gt;, and then by &lt;code&gt;salary&lt;/code&gt; within each department&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example, consider these employee records:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;employee_id&lt;/th&gt;
&lt;th&gt;department&lt;/th&gt;
&lt;th&gt;salary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Engineering&lt;/td&gt;
&lt;td&gt;70000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;HR&lt;/td&gt;
&lt;td&gt;45000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Engineering&lt;/td&gt;
&lt;td&gt;50000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Sales&lt;/td&gt;
&lt;td&gt;60000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;HR&lt;/td&gt;
&lt;td&gt;55000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Engineering&lt;/td&gt;
&lt;td&gt;90000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The composite index on &lt;code&gt;(department, salary)&lt;/code&gt; organizes its entries in this order:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;department&lt;/th&gt;
&lt;th&gt;salary&lt;/th&gt;
&lt;th&gt;Employee&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Engineering&lt;/td&gt;
&lt;td&gt;50000&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engineering&lt;/td&gt;
&lt;td&gt;70000&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engineering&lt;/td&gt;
&lt;td&gt;90000&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HR&lt;/td&gt;
&lt;td&gt;45000&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HR&lt;/td&gt;
&lt;td&gt;55000&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sales&lt;/td&gt;
&lt;td&gt;60000&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice that the employees are grouped by department. Within each department, their salaries are arranged in ascending order.&lt;/p&gt;

&lt;p&gt;This ordering is important because PostgreSQL can use it to narrow down the records it needs to examine.&lt;/p&gt;

&lt;p&gt;Also, &lt;strong&gt;column order matters&lt;/strong&gt;. An index on &lt;code&gt;(department, salary)&lt;/code&gt; is not organized in the same way as an index on &lt;code&gt;(salary, department)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Index Behavior with an Example
&lt;/h2&gt;

&lt;p&gt;Let's use an &lt;code&gt;employees&lt;/code&gt; table to understand how PostgreSQL handles queries with multiple filtering conditions.&lt;/p&gt;

&lt;p&gt;First, we'll create 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;TABLE&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;employee_id&lt;/span&gt; &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;employee_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's insert 1 million employee records.&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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;employee_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="s1"&gt;'Employee_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Engineering'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'HR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Sales'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Finance'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Marketing'&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="mi"&gt;30000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;g&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll compare three index configurations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Case 1:&lt;/strong&gt; Two separate indexes on &lt;code&gt;department&lt;/code&gt; and &lt;code&gt;salary&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Case 2:&lt;/strong&gt; A composite index on &lt;code&gt;(department, salary)&lt;/code&gt;, after removing the separate indexes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Case 3:&lt;/strong&gt; A composite index on &lt;code&gt;(department, salary)&lt;/code&gt; along with a separate index on &lt;code&gt;salary&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each configuration, we'll execute the same four queries and observe the execution plans and performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 1: Creating Two Separate Indexes
&lt;/h2&gt;

&lt;p&gt;Let's start by creating two individual indexes on the &lt;code&gt;employees&lt;/code&gt; 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_employees_department&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;department&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&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_employees_salary&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;salary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's execute a few queries and see how PostgreSQL uses these indexes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 1: Filtering by Department
&lt;/h3&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="n"&gt;employee_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Engineering'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Plan:&lt;/strong&gt;&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%2F9hr4r1n550s1wve7xass.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%2F9hr4r1n550s1wve7xass.png" alt=" " width="800" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL used a &lt;strong&gt;Bitmap Index Scan&lt;/strong&gt; on &lt;code&gt;idx_employees_department&lt;/code&gt;, followed by a &lt;strong&gt;Bitmap Heap Scan&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A Bitmap Index Scan first identifies the locations of matching records using the index. PostgreSQL then uses those locations to access the required heap pages through a Bitmap Heap Scan.&lt;/p&gt;

&lt;p&gt;Here, the query returned &lt;strong&gt;200,000 records&lt;/strong&gt; by accessing &lt;strong&gt;8,161 heap pages&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution Time: 123.040 ms&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 2: Filtering by Salary
&lt;/h3&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="n"&gt;employee_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;70000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Plan:&lt;/strong&gt;&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%2Fqctjg8y0djbgj1szfupe.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%2Fqctjg8y0djbgj1szfupe.png" alt=" " width="800" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This time, PostgreSQL used &lt;code&gt;idx_employees_salary&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The execution plan again contained a Bitmap Index Scan followed by a Bitmap Heap Scan.&lt;/p&gt;

&lt;p&gt;The query returned &lt;strong&gt;10,000 records&lt;/strong&gt;, accessing &lt;strong&gt;8,161 heap pages&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Execution Time: 130.689 ms&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 3: Filtering Using AND
&lt;/h3&gt;

&lt;p&gt;Now, let's filter employees using both &lt;code&gt;department&lt;/code&gt; and &lt;code&gt;salary&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;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="n"&gt;employee_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Engineering'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;70000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Plan:&lt;/strong&gt;&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%2F6meva6d5c4srsirze5h4.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%2F6meva6d5c4srsirze5h4.png" alt=" " width="800" height="249"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL used two &lt;strong&gt;Bitmap Index Scans&lt;/strong&gt;, combined their results using &lt;strong&gt;BitmapAnd&lt;/strong&gt;, and then performed a &lt;strong&gt;Bitmap Heap Scan&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  How does BitmapAnd work?
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;PostgreSQL searches &lt;code&gt;idx_employees_department&lt;/code&gt; and identifies &lt;strong&gt;200,000 matching records&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;It searches &lt;code&gt;idx_employees_salary&lt;/code&gt; and identifies &lt;strong&gt;10,000 matching records&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BitmapAnd&lt;/strong&gt; combines the two bitmaps, keeping only the row locations that satisfy both conditions.&lt;/li&gt;
&lt;li&gt;The Bitmap Heap Scan uses the resulting bitmap to fetch matching rows from the table.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example, suppose the two indexes identify these employee IDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Department = Engineering → 1, 3, 5, 7
Salary = 70000          → 2, 3, 5, 8

BitmapAnd                 → 3, 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These IDs are just an example to illustrate the intersection. PostgreSQL's bitmaps represent heap tuple locations, not employee IDs directly.&lt;/p&gt;

&lt;p&gt;In our actual execution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rows returned:&lt;/strong&gt; 10,000&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heap pages accessed:&lt;/strong&gt; 8,161&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution time:&lt;/strong&gt; 117.610 ms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice that PostgreSQL scanned both indexes separately before combining their results. We will compare this behavior with a composite index in the next case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 4: Filtering Using OR
&lt;/h3&gt;

&lt;p&gt;Now, let's find employees who belong to the Engineering department &lt;strong&gt;or&lt;/strong&gt; have a salary of 70,000.&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="n"&gt;employee_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Engineering'&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;70000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Plan:&lt;/strong&gt;&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%2F7qqezp28k4cj7ns24fcz.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%2F7qqezp28k4cj7ns24fcz.png" alt=" " width="800" height="249"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL used two &lt;strong&gt;Bitmap Index Scans&lt;/strong&gt;, combined their results using &lt;strong&gt;BitmapOr&lt;/strong&gt;, and then performed a &lt;strong&gt;Bitmap Heap Scan&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  How does BitmapOr work?
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;PostgreSQL searches &lt;code&gt;idx_employees_department&lt;/code&gt; and identifies 200,000 matching records.&lt;/li&gt;
&lt;li&gt;It searches &lt;code&gt;idx_employees_salary&lt;/code&gt; and identifies 10,000 matching records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BitmapOr&lt;/strong&gt; combines the two bitmaps, keeping row locations that match either condition.&lt;/li&gt;
&lt;li&gt;The Bitmap Heap Scan visits the corresponding heap pages and retrieves the matching employee IDs.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Department = Engineering → 1, 3, 5, 7
Salary = 70000          → 2, 3, 5, 8

BitmapOr                  → 1, 2, 3, 5, 7, 8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL combines heap tuple locations rather than employee IDs directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Actual Results:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rows returned: &lt;strong&gt;200,000&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Heap pages accessed: &lt;strong&gt;8,161&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Execution time: &lt;strong&gt;126.784 ms&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice that the query returned 200,000 records, not 210,000. In our generated dataset, all employees earning 70,000 belong to the Engineering department, so those records already satisfy the first condition.&lt;/p&gt;

&lt;p&gt;PostgreSQL used both indexes and combined their results without returning duplicate rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 2: Creating a Composite Index
&lt;/h2&gt;

&lt;p&gt;Now, let's remove the two separate indexes and create a composite index on &lt;code&gt;department&lt;/code&gt; and &lt;code&gt;salary&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;DROP&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_employees_department&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_employees_salary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the 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_employees_department_salary&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;department&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's run the same four queries and observe how PostgreSQL uses this index.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 1: Filtering by Department
&lt;/h3&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="n"&gt;employee_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Engineering'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Plan:&lt;/strong&gt;&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%2Ftqaj1z255usxflwdgryc.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%2Ftqaj1z255usxflwdgryc.png" alt=" " width="800" height="196"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL used a &lt;strong&gt;Bitmap Index Scan&lt;/strong&gt; on &lt;code&gt;idx_employees_department_salary&lt;/code&gt;, followed by a &lt;strong&gt;Bitmap Heap Scan&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PostgreSQL searches the composite index using &lt;code&gt;department = 'Engineering'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Since &lt;code&gt;department&lt;/code&gt; is the first column in the index, PostgreSQL can directly locate the matching index entries without requiring a condition on &lt;code&gt;salary&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Bitmap Index Scan collects the matching heap tuple locations into a bitmap.&lt;/li&gt;
&lt;li&gt;The Bitmap Heap Scan visits the corresponding heap pages, checks tuple visibility, and retrieves the employee IDs. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rows returned: &lt;strong&gt;200,000&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Heap pages accessed: &lt;strong&gt;8,161&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Execution time: &lt;strong&gt;130.189 ms&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even though the index contains two columns, PostgreSQL was able to use it with a condition on the first column alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 2: Filtering by Salary
&lt;/h3&gt;

&lt;p&gt;Now, let's filter employees using only &lt;code&gt;salary&lt;/code&gt;, which is the second column in our composite index &lt;code&gt;(department, salary)&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;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="n"&gt;employee_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;70000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Plan:&lt;/strong&gt;&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%2F39er6bqw60pmz20z15nl.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%2F39er6bqw60pmz20z15nl.png" alt=" " width="800" height="172"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This time, PostgreSQL chose a &lt;strong&gt;Parallel Sequential Scan&lt;/strong&gt; instead of using the composite index.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rows returned:&lt;/strong&gt; 10,000&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heap pages read:&lt;/strong&gt; 8,161&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workers launched:&lt;/strong&gt; 2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution time:&lt;/strong&gt; 98.216 ms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostgreSQL scanned the table using three processes (the main process and two parallel workers), checking the salary condition against the rows.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why didn't PostgreSQL use the composite index?
&lt;/h4&gt;

&lt;p&gt;Our composite index is created on:&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_employees_department_salary&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;department&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL organizes the index entries first by &lt;code&gt;department&lt;/code&gt;, then by &lt;code&gt;salary&lt;/code&gt; within each department.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Department&lt;/th&gt;
&lt;th&gt;Salary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Engineering&lt;/td&gt;
&lt;td&gt;50000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engineering&lt;/td&gt;
&lt;td&gt;70000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engineering&lt;/td&gt;
&lt;td&gt;90000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finance&lt;/td&gt;
&lt;td&gt;60000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finance&lt;/td&gt;
&lt;td&gt;70000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HR&lt;/td&gt;
&lt;td&gt;45000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HR&lt;/td&gt;
&lt;td&gt;70000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sales&lt;/td&gt;
&lt;td&gt;55000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sales&lt;/td&gt;
&lt;td&gt;70000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice that employees earning 70,000 are spread across different departments in the index.&lt;/p&gt;

&lt;p&gt;When we filter by &lt;code&gt;department&lt;/code&gt;, PostgreSQL can directly locate the corresponding entries because it is the first indexed column.&lt;/p&gt;

&lt;p&gt;But when we filter only by &lt;code&gt;salary&lt;/code&gt;, the matching values are scattered across different departments. PostgreSQL cannot retrieve all matching entries through a single continuous range of the index.&lt;/p&gt;

&lt;p&gt;This behavior is commonly described by the &lt;strong&gt;leftmost prefix rule&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An index on &lt;code&gt;(department, salary)&lt;/code&gt; efficiently supports filtering by &lt;code&gt;department&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It also supports filtering by both &lt;code&gt;department&lt;/code&gt; and &lt;code&gt;salary&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Filtering only by &lt;code&gt;salary&lt;/code&gt; is generally less efficient because the leading column is missing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostgreSQL can sometimes use a composite index even without a condition on its first column, but whether that is worthwhile depends on the data and estimated query cost.&lt;/p&gt;

&lt;p&gt;In our execution, PostgreSQL chose a Parallel Sequential Scan rather than using the composite index.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 3: Filtering Using AND
&lt;/h3&gt;

&lt;p&gt;Now, let's filter employees using both &lt;code&gt;department&lt;/code&gt; and &lt;code&gt;salary&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;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="n"&gt;employee_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Engineering'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;70000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Plan:&lt;/strong&gt;&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%2Ftg5hsgdt0sio2us1791h.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%2Ftg5hsgdt0sio2us1791h.png" alt=" " width="800" height="218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL used a &lt;strong&gt;Bitmap Index Scan&lt;/strong&gt; on &lt;code&gt;idx_employees_department_salary&lt;/code&gt;, followed by a &lt;strong&gt;Bitmap Heap Scan&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Unlike Case 1, where PostgreSQL used two separate indexes and combined their results using &lt;code&gt;BitmapAnd&lt;/code&gt;, this time it used a single composite index.&lt;/p&gt;

&lt;p&gt;Here's how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PostgreSQL searches the composite index for entries where &lt;code&gt;department = 'Engineering'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Since salary values are ordered within each department, PostgreSQL can further narrow the search to entries where &lt;code&gt;salary = 70000&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Bitmap Index Scan collects the matching heap tuple locations into a bitmap.&lt;/li&gt;
&lt;li&gt;The Bitmap Heap Scan visits the corresponding heap pages, checks tuple visibility, and retrieves the matching employee IDs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rows returned: &lt;strong&gt;10,000&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Heap pages accessed: &lt;strong&gt;8,161&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Execution time: &lt;strong&gt;115.172 ms&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostgreSQL was able to use both filtering conditions directly within a single composite index.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 4: Filtering Using OR
&lt;/h3&gt;

&lt;p&gt;Now, let's filter employees using either &lt;code&gt;department&lt;/code&gt; or &lt;code&gt;salary&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;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="n"&gt;employee_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Engineering'&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;70000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Plan:&lt;/strong&gt;&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%2Ftya2qsznj8kqiy5eumlc.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%2Ftya2qsznj8kqiy5eumlc.png" alt=" " width="800" height="187"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This time, PostgreSQL chose a &lt;strong&gt;Sequential Scan&lt;/strong&gt; instead of using the composite index.&lt;/p&gt;

&lt;p&gt;Here's why:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Our composite index is created on &lt;code&gt;(department, salary)&lt;/code&gt;, where &lt;code&gt;department&lt;/code&gt; is the first column.&lt;/li&gt;
&lt;li&gt;PostgreSQL can efficiently search using &lt;code&gt;department&lt;/code&gt;, but the &lt;code&gt;OR&lt;/code&gt; condition also requires finding employees with &lt;code&gt;salary = 70000&lt;/code&gt;, regardless of their department.&lt;/li&gt;
&lt;li&gt;Since &lt;code&gt;salary&lt;/code&gt; is the second column, its matching values are spread across different departments in the index.&lt;/li&gt;
&lt;li&gt;PostgreSQL estimated that scanning the table and checking both conditions would be cheaper than using the composite index.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rows returned: &lt;strong&gt;200,000&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Rows removed by filter: &lt;strong&gt;800,000&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Heap pages read: &lt;strong&gt;8,161&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Execution time: &lt;strong&gt;138.981 ms&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unlike Case 1, where PostgreSQL combined two separate indexes using &lt;code&gt;BitmapOr&lt;/code&gt;, here it performed a Sequential Scan because there was no separate index on &lt;code&gt;salary&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case 3: Composite Index with a Separate Salary Index
&lt;/h2&gt;

&lt;p&gt;We have kept the composite index on &lt;code&gt;(department, salary)&lt;/code&gt; and created an additional index on &lt;code&gt;salary&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_employees_salary&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;salary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's run the same four queries again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 1: Filtering by Department
&lt;/h3&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="n"&gt;employee_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Engineering'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Plan:&lt;/strong&gt;&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%2Fecjdtlndqga2c8dtcmaf.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%2Fecjdtlndqga2c8dtcmaf.png" alt=" " width="799" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL used a &lt;strong&gt;Bitmap Index Scan&lt;/strong&gt; on &lt;code&gt;idx_employees_department_salary&lt;/code&gt;, followed by a &lt;strong&gt;Bitmap Heap Scan&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PostgreSQL searches the composite index using &lt;code&gt;department = 'Engineering'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Since &lt;code&gt;department&lt;/code&gt; is the first column in the index, PostgreSQL can locate the matching entries without requiring a condition on &lt;code&gt;salary&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Bitmap Index Scan collects the matching heap tuple locations.&lt;/li&gt;
&lt;li&gt;The Bitmap Heap Scan visits the corresponding heap pages, checks tuple visibility, and retrieves the employee IDs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rows returned: &lt;strong&gt;200,000&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Heap pages accessed: &lt;strong&gt;8,161&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Execution time: &lt;strong&gt;127.859 ms&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostgreSQL continued to use the composite index for this query. The separate &lt;code&gt;salary&lt;/code&gt; index was not needed because the query filtered only on &lt;code&gt;department&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 2: Filtering by Salary
&lt;/h3&gt;

&lt;p&gt;Now, let's filter employees using only &lt;code&gt;salary&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;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="n"&gt;employee_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;70000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Plan:&lt;/strong&gt;&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%2Fmn49x1vb0jcfawjah9pm.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%2Fmn49x1vb0jcfawjah9pm.png" alt=" " width="799" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL used a &lt;strong&gt;Bitmap Index Scan&lt;/strong&gt; on &lt;code&gt;idx_employees_salary&lt;/code&gt;, followed by a &lt;strong&gt;Bitmap Heap Scan&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PostgreSQL searches the separate &lt;code&gt;salary&lt;/code&gt; index for entries where &lt;code&gt;salary = 70000&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Bitmap Index Scan collects the matching heap tuple locations into a bitmap.&lt;/li&gt;
&lt;li&gt;The Bitmap Heap Scan visits the corresponding heap pages, checks tuple visibility, and retrieves the matching employee IDs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rows returned: &lt;strong&gt;10,000&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Heap pages accessed: &lt;strong&gt;8,161&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Execution time: &lt;strong&gt;12.315 ms&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Case 2, PostgreSQL chose a Parallel Sequential Scan because only the composite index &lt;code&gt;(department, salary)&lt;/code&gt; was available.&lt;/p&gt;

&lt;p&gt;Now that we have a separate index on &lt;code&gt;salary&lt;/code&gt;, PostgreSQL can directly search for matching salary values without depending on the leading &lt;code&gt;department&lt;/code&gt; column.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 3: Filtering Using AND
&lt;/h3&gt;

&lt;p&gt;Now, let's filter employees using both &lt;code&gt;department&lt;/code&gt; and &lt;code&gt;salary&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;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="n"&gt;employee_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Engineering'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;70000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Plan:&lt;/strong&gt;&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%2Ftthm4ixtbmw8y14af050.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%2Ftthm4ixtbmw8y14af050.png" alt=" " width="800" height="218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL used a &lt;strong&gt;Bitmap Index Scan&lt;/strong&gt; on &lt;code&gt;idx_employees_department_salary&lt;/code&gt;, followed by a &lt;strong&gt;Bitmap Heap Scan&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Even though we have a separate index on &lt;code&gt;salary&lt;/code&gt;, PostgreSQL chose to use only the composite index.&lt;/p&gt;

&lt;p&gt;Here's how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PostgreSQL searches the composite index for entries where &lt;code&gt;department = 'Engineering'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Since salary values are ordered within each department, PostgreSQL can further narrow the search using &lt;code&gt;salary = 70000&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Bitmap Index Scan collects the matching heap tuple locations into a bitmap.&lt;/li&gt;
&lt;li&gt;The Bitmap Heap Scan visits the corresponding heap pages, checks tuple visibility, and retrieves the matching employee IDs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rows returned: &lt;strong&gt;10,000&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Heap pages accessed: &lt;strong&gt;8,161&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Execution time: &lt;strong&gt;94.164 ms&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unlike Case 1, where PostgreSQL combined two separate indexes using &lt;code&gt;BitmapAnd&lt;/code&gt;, here it used a single composite index to apply both filtering conditions.&lt;/p&gt;

&lt;p&gt;The separate salary index was not needed for this query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 4: Filtering Using OR
&lt;/h3&gt;

&lt;p&gt;Now, let's filter employees using either &lt;code&gt;department&lt;/code&gt; or &lt;code&gt;salary&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;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="n"&gt;employee_id&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;department&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Engineering'&lt;/span&gt;
   &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;70000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Execution Plan:&lt;/strong&gt;&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%2Feqs3hzt759t707le8vng.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%2Feqs3hzt759t707le8vng.png" alt=" " width="800" height="239"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL used two &lt;strong&gt;Bitmap Index Scans&lt;/strong&gt;, combined their results using &lt;strong&gt;BitmapOr&lt;/strong&gt;, and then performed a &lt;strong&gt;Bitmap Heap Scan&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This time, PostgreSQL used both the composite index &lt;code&gt;idx_employees_department_salary&lt;/code&gt; and the separate index &lt;code&gt;idx_employees_salary&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;PostgreSQL searches the composite index using &lt;code&gt;department = 'Engineering'&lt;/code&gt; and identifies 200,000 matching records.&lt;/li&gt;
&lt;li&gt;It searches the separate salary index using &lt;code&gt;salary = 70000&lt;/code&gt; and identifies 10,000 matching records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BitmapOr&lt;/strong&gt; combines both bitmaps, keeping the heap tuple locations that match either condition.&lt;/li&gt;
&lt;li&gt;The Bitmap Heap Scan visits the corresponding heap pages, checks tuple visibility, and retrieves the matching employee IDs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Results:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rows returned: &lt;strong&gt;200,000&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Heap pages accessed: &lt;strong&gt;8,161&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Execution time: &lt;strong&gt;63.308 ms&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Case 2, PostgreSQL performed a Sequential Scan for the same &lt;code&gt;OR&lt;/code&gt; query because only the composite index was available.&lt;/p&gt;

&lt;p&gt;With the additional salary index, PostgreSQL was able to use &lt;code&gt;BitmapOr&lt;/code&gt; to combine both indexes and retrieve the matching records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing the Three Index Configurations
&lt;/h2&gt;

&lt;p&gt;We executed the same four queries using different index configurations.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;Separate Indexes&lt;/th&gt;
&lt;th&gt;Composite Index&lt;/th&gt;
&lt;th&gt;Composite + Salary Index&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Department&lt;/td&gt;
&lt;td&gt;Bitmap Index Scan&lt;/td&gt;
&lt;td&gt;Bitmap Index Scan&lt;/td&gt;
&lt;td&gt;Bitmap Index Scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Salary&lt;/td&gt;
&lt;td&gt;Bitmap Index Scan&lt;/td&gt;
&lt;td&gt;Parallel Seq Scan&lt;/td&gt;
&lt;td&gt;Bitmap Index Scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Department AND Salary&lt;/td&gt;
&lt;td&gt;BitmapAnd&lt;/td&gt;
&lt;td&gt;Bitmap Index Scan&lt;/td&gt;
&lt;td&gt;Bitmap Index Scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Department OR Salary&lt;/td&gt;
&lt;td&gt;BitmapOr&lt;/td&gt;
&lt;td&gt;Seq Scan&lt;/td&gt;
&lt;td&gt;BitmapOr&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Composite indexes are useful when queries frequently filter or sort using multiple columns.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Column order matters:&lt;/strong&gt; The order of columns in a composite index determines how efficiently PostgreSQL can use it for searching and sorting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leftmost prefix rule:&lt;/strong&gt; PostgreSQL generally uses composite indexes most efficiently when queries filter using the leading indexed columns. Filtering only by a non-leading column may be less efficient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AND conditions:&lt;/strong&gt; A composite index can use multiple filtering conditions in a single index scan, avoiding the need to combine separate indexes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OR conditions:&lt;/strong&gt; PostgreSQL can combine multiple indexes using &lt;code&gt;BitmapOr&lt;/code&gt; when suitable indexes are available. Otherwise, it may choose a Sequential Scan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate indexes:&lt;/strong&gt; A composite index may not replace the need for individual indexes, especially when non-leading columns are frequently filtered independently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Composite indexes can improve query performance when columns are indexed in the right order. However, no single index works best for every query, so choose indexes based on your most frequently used filtering and sorting conditions.&lt;/p&gt;

</description>
      <category>database</category>
      <category>performance</category>
      <category>postgres</category>
      <category>sql</category>
    </item>
    <item>
      <title>Database Indexing – Part 2: Understanding Covering Indexes in PostgreSQL</title>
      <dc:creator>Anusha Renangi</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:34:37 +0000</pubDate>
      <link>https://dev.to/anusha_renangi_05a4e41d35/database-indexing-part-2-understanding-covering-indexes-in-postgresql-17ee</link>
      <guid>https://dev.to/anusha_renangi_05a4e41d35/database-indexing-part-2-understanding-covering-indexes-in-postgresql-17ee</guid>
      <description>&lt;p&gt;Recently, while working on a performance optimization task, I came across an interesting problem.&lt;/p&gt;

&lt;p&gt;I had a query where I needed to fetch document versions for around &lt;strong&gt;6,500 document IDs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Something like 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;SELECT&lt;/span&gt; &lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;document_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;102&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;103&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt; &lt;span class="mi"&gt;6500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks like a simple query, right?&lt;/p&gt;

&lt;p&gt;But the entire operation was taking around &lt;strong&gt;4 seconds&lt;/strong&gt;. When I checked where the time was being spent, I noticed that this single database query was taking almost &lt;strong&gt;85% of the total time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's around 3.4 seconds for just one database query!&lt;/p&gt;

&lt;p&gt;So, I started looking for ways to reduce the query execution time.&lt;/p&gt;

&lt;p&gt;My first thought was to create an index on both &lt;code&gt;document_id&lt;/code&gt; and &lt;code&gt;document_version&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_document_version&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But I'm filtering only using &lt;code&gt;document_id&lt;/code&gt;. I just need &lt;code&gt;document_version&lt;/code&gt; in the result.&lt;/p&gt;

&lt;p&gt;Also, &lt;code&gt;document_version&lt;/code&gt; changes frequently, so I was concerned about the additional index maintenance during updates.&lt;/p&gt;

&lt;p&gt;I wanted to know whether both columns really needed to be part of the index key.&lt;/p&gt;

&lt;p&gt;PostgreSQL also has an &lt;code&gt;INCLUDE&lt;/code&gt; option, where we can store additional columns in an index without making them key 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="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_document_version&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both indexes contain the columns required by my query. But how are they different internally? Does using &lt;code&gt;INCLUDE&lt;/code&gt; make any difference to query performance, index size, or updates?&lt;/p&gt;

&lt;p&gt;Let's look at how both indexes work.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Understanding Covering Indexes
&lt;/h2&gt;

&lt;p&gt;A covering index is an index that contains all the columns required by a query, so PostgreSQL may be able to return the result without fetching the actual row from the table.&lt;/p&gt;

&lt;p&gt;For example, consider 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="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;document_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we're using &lt;code&gt;document_id&lt;/code&gt; to search and &lt;code&gt;document_version&lt;/code&gt; to get the required value.&lt;/p&gt;

&lt;p&gt;If we have an index only on &lt;code&gt;document_id&lt;/code&gt;, PostgreSQL can use it to find the matching record. But it still needs to visit the heap to fetch &lt;code&gt;document_version&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What if both values were already available in the index?&lt;/p&gt;

&lt;p&gt;We could create an index like 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;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_document_version&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we have two types of columns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Key Column — &lt;code&gt;document_id&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Used to search and navigate through the B-tree.&lt;/li&gt;
&lt;li&gt;Determines the ordering of index entries.&lt;/li&gt;
&lt;li&gt;Can be used for filtering and sorting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Non-Key Column — &lt;code&gt;document_version&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stored along with the key in the leaf pages.&lt;/li&gt;
&lt;li&gt;Doesn't participate in B-tree searching or ordering.&lt;/li&gt;
&lt;li&gt;Can be returned from the index without fetching that value from the heap.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How Are These Columns Stored?
&lt;/h3&gt;

&lt;p&gt;PostgreSQL's B-tree has three types of pages: root, internal, and leaf pages.&lt;/p&gt;

&lt;p&gt;In an index created using &lt;code&gt;INCLUDE&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Root and internal pages&lt;/strong&gt; contain navigation keys based on &lt;code&gt;document_id&lt;/code&gt; and pointers to lower-level pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaf pages&lt;/strong&gt; contain &lt;code&gt;document_id&lt;/code&gt;, the included &lt;code&gt;document_version&lt;/code&gt;, and a reference to the corresponding heap tuple.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, PostgreSQL searches the B-tree using &lt;code&gt;document_id&lt;/code&gt; and finds &lt;code&gt;document_version&lt;/code&gt; in the matching leaf entry.&lt;/p&gt;

&lt;p&gt;Since both required columns are available in the index, PostgreSQL may be able to return the result without fetching the actual row from the heap. This is called an &lt;strong&gt;Index Only Scan&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Testing Covering Indexes in PostgreSQL
&lt;/h2&gt;

&lt;p&gt;Let's create a &lt;code&gt;documents&lt;/code&gt; table with 10 million records and compare how PostgreSQL performs with a primary key index and an index using &lt;code&gt;INCLUDE&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create the Table
&lt;/h3&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;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;document_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;document_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;document_version&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since &lt;code&gt;document_id&lt;/code&gt; is the primary key, PostgreSQL automatically creates a B-tree index named &lt;code&gt;documents_pkey&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Insert 10 Million Records
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;document_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;document_version&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;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'Document_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10000000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, our table contains 10 million documents.&lt;/p&gt;

&lt;p&gt;Let's fetch document versions for 6,500 document IDs and compare both approaches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 1: Using the Primary Key Index
&lt;/h3&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="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;document_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6500&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;BIGINT&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;&lt;strong&gt;Output:&lt;/strong&gt;&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%2Fqp60r4s7d58hdvn8q1fi.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%2Fqp60r4s7d58hdvn8q1fi.png" alt=" " width="799" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL used an &lt;strong&gt;Index Scan&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The primary key index contains &lt;code&gt;document_id&lt;/code&gt;, but not &lt;code&gt;document_version&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So PostgreSQL:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Searches the index for the matching document IDs.&lt;/li&gt;
&lt;li&gt;Gets the corresponding heap tuple references.&lt;/li&gt;
&lt;li&gt;Visits the heap to fetch &lt;code&gt;document_version&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The query returned 6,500 records and took &lt;strong&gt;11.028 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 2: Creating an Index with INCLUDE
&lt;/h3&gt;

&lt;p&gt;Now, let's create an index that also contains &lt;code&gt;document_version&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_documents_covering&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;INCLUDE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;document_id&lt;/code&gt; is the key column, while &lt;code&gt;document_version&lt;/code&gt; is a non-key column stored in the leaf pages.&lt;/p&gt;

&lt;p&gt;Let's execute the same query again.&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="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;document_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6500&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;BIGINT&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;&lt;strong&gt;Output:&lt;/strong&gt;&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%2Fthsisutm0alz1omg8mx5.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%2Fthsisutm0alz1omg8mx5.png" alt=" " width="800" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This time, PostgreSQL used an &lt;strong&gt;Index Only Scan&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Since both &lt;code&gt;document_id&lt;/code&gt; and &lt;code&gt;document_version&lt;/code&gt; are available in the index, PostgreSQL was able to return the required values without visiting the heap in this execution.&lt;/p&gt;

&lt;p&gt;Notice:&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;The query took &lt;strong&gt;9.144 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparing the Execution Times
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Primary Key Index&lt;/th&gt;
&lt;th&gt;Index with INCLUDE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scan type&lt;/td&gt;
&lt;td&gt;Index Scan&lt;/td&gt;
&lt;td&gt;Index Only Scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rows returned&lt;/td&gt;
&lt;td&gt;6,500&lt;/td&gt;
&lt;td&gt;6,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution time&lt;/td&gt;
&lt;td&gt;11.028 ms&lt;/td&gt;
&lt;td&gt;9.144 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In this experiment, the execution time decreased from &lt;strong&gt;11.028 ms to 9.144 ms&lt;/strong&gt;, approximately a &lt;strong&gt;17% improvement&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With the primary key index, PostgreSQL had to visit the heap to fetch &lt;code&gt;document_version&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But with &lt;code&gt;INCLUDE&lt;/code&gt;, both required columns were available in the index, and PostgreSQL avoided those heap fetches.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Note on Caching
&lt;/h3&gt;

&lt;p&gt;I tested both approaches on my local Ubuntu machine. Before each test, I restarted PostgreSQL and cleared the Linux filesystem cache using the following commands:&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="nb"&gt;sudo &lt;/span&gt;systemctl stop postgresql
&lt;span class="nb"&gt;sync
echo &lt;/span&gt;3 | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /proc/sys/vm/drop_caches
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start postgresql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These commands are only intended for a dedicated local test environment, not a production or shared server.&lt;/p&gt;

&lt;p&gt;The covering index improved query performance in our experiment by avoiding heap fetches.&lt;/p&gt;

&lt;p&gt;However, since we can't completely eliminate caching effects, it's difficult to say exactly how much improvement we can expect.&lt;/p&gt;

&lt;p&gt;These results are from my local system, and the actual performance improvement may vary depending on the environment and workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Why Can an Index Only Scan Still Have Heap Fetches?
&lt;/h2&gt;

&lt;p&gt;In our previous query, PostgreSQL used an Index Only Scan with &lt;code&gt;Heap Fetches: 0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But an Index Only Scan doesn't always mean PostgreSQL will completely avoid accessing the heap.&lt;/p&gt;

&lt;p&gt;Let's update some records and check.&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;document_id&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;6500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's execute the same query again.&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="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;document_version&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;document_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6500&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;BIGINT&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;&lt;strong&gt;Output:&lt;/strong&gt;&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%2Fv7s0qtm7sveayggcfh0m.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%2Fv7s0qtm7sveayggcfh0m.png" alt=" " width="800" height="242"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL still used an Index Only Scan, but this time:&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: 13000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We updated 6,500 records, but PostgreSQL performed 13,000 heap fetches.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  How Does PostgreSQL Check Row Visibility?
&lt;/h3&gt;

&lt;p&gt;PostgreSQL uses &lt;strong&gt;MVCC (Multi-Version Concurrency Control)&lt;/strong&gt; to manage updates and transactions.&lt;/p&gt;

&lt;p&gt;When we update a row, PostgreSQL creates a new row version instead of overwriting the existing one.&lt;/p&gt;

&lt;p&gt;The index contains the values we need, but it doesn't have enough information to determine whether a row version is visible to the current transaction.&lt;/p&gt;

&lt;p&gt;For this, PostgreSQL maintains a &lt;strong&gt;Visibility Map&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The visibility map keeps track of which heap pages contain only row versions that are visible to all transactions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If a heap page is marked &lt;strong&gt;all-visible&lt;/strong&gt;, PostgreSQL can return the required values from the index without checking the heap.&lt;/li&gt;
&lt;li&gt;If a heap page isn't marked all-visible, PostgreSQL needs to visit the heap to check row visibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What Happened After the UPDATE?
&lt;/h3&gt;

&lt;p&gt;When we updated those 6,500 records, PostgreSQL created new row versions and cleared the all-visible flags for the affected heap pages.&lt;/p&gt;

&lt;p&gt;Since &lt;code&gt;document_version&lt;/code&gt; is included in our index, the update also required new index entries.&lt;/p&gt;

&lt;p&gt;During the next Index Only Scan, PostgreSQL had to check the heap for row visibility.&lt;/p&gt;

&lt;p&gt;That's why we got:&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: 13000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are heap tuple fetch attempts, not 13,000 different heap pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  What About VACUUM?
&lt;/h3&gt;

&lt;p&gt;PostgreSQL provides &lt;code&gt;VACUUM&lt;/code&gt; to clean up dead tuples so their space can be reused. It also updates the visibility map when pages qualify as all-visible.&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="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can help reduce heap fetches during Index Only Scans, although it doesn't guarantee that they will become zero.&lt;/p&gt;

&lt;p&gt;We'll cover how &lt;code&gt;VACUUM&lt;/code&gt; works internally in a separate article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Covering indexes can help reduce heap access when all the required columns are available in the index.&lt;/p&gt;

&lt;p&gt;But indexes also come with a cost.&lt;/p&gt;

&lt;p&gt;Even though &lt;code&gt;INCLUDE&lt;/code&gt; columns are not part of the index key, PostgreSQL still needs to store and maintain their values. If an included column is updated frequently, it can increase write overhead.&lt;/p&gt;

&lt;p&gt;So, we need to consider both read and write performance before creating additional indexes.&lt;/p&gt;

&lt;p&gt;In the next article, we'll look at &lt;strong&gt;composite indexes&lt;/strong&gt;, how column order affects index usage, and why the order of columns matters when filtering and sorting records.&lt;/p&gt;

&lt;p&gt;That's it for now!&lt;/p&gt;

&lt;p&gt;Thanks for reading! 😊&lt;/p&gt;

</description>
      <category>database</category>
      <category>performance</category>
      <category>postgres</category>
      <category>sql</category>
    </item>
    <item>
      <title>Database Indexing – Part 1: How Indexes Help Find Data Faster</title>
      <dc:creator>Anusha Renangi</dc:creator>
      <pubDate>Sat, 17 Oct 2026 15:10:09 +0000</pubDate>
      <link>https://dev.to/anusha_renangi_05a4e41d35/database-indexing-part-1-how-indexes-help-find-data-faster-2pk5</link>
      <guid>https://dev.to/anusha_renangi_05a4e41d35/database-indexing-part-1-how-indexes-help-find-data-faster-2pk5</guid>
      <description>&lt;p&gt;Recently, I was reading two books: &lt;em&gt;Good Vibes, Good Life&lt;/em&gt; and &lt;em&gt;The Mountain Is You&lt;/em&gt;.&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%2Fsqtrxco4am6b8iolsacj.jpeg" 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%2Fsqtrxco4am6b8iolsacj.jpeg" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I usually forget things after some time 😅. So whenever I want to revisit something I've read, I go back to the book.&lt;/p&gt;

&lt;p&gt;But I noticed one difference between these two books.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good Vibes, Good Life&lt;/strong&gt; has an index at the end of the book. Whenever I want to read about a particular topic, I can look at the index, find the page number, and directly go to that page.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;The Mountain Is You&lt;/strong&gt; doesn't have that kind of index.&lt;/p&gt;

&lt;p&gt;So if I remember a topic but don't remember where I read it, I have to go through the chapters one by one until I find it.&lt;/p&gt;

&lt;p&gt;That takes more time, right?&lt;/p&gt;

&lt;p&gt;The same thing happens in databases.&lt;/p&gt;

&lt;p&gt;Imagine we have a table containing millions of records, and we want to find one particular record.&lt;/p&gt;

&lt;p&gt;Without an index, the database may need to scan the entire table, checking rows to find the data we're looking for.&lt;/p&gt;

&lt;p&gt;But if we have a suitable index, the database can use it to quickly locate the required record instead of checking every row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is why we use indexes in databases.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An index is a separate data structure that helps the database find records efficiently, just like the index in a book helps us find a topic without going through every page.&lt;/p&gt;

&lt;p&gt;Before moving forward, check out my previous article, &lt;a href="https://dev.to/anusha_renangi_05a4e41d35/database-storage-concepts-how-tables-are-stored-on-disk-387h"&gt;Database Storage Concepts – How Tables Are Stored on Disk&lt;/a&gt;, to understand how databases store data internally.&lt;/p&gt;

&lt;p&gt;Now, let's see how indexing actually works in PostgreSQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a Table with an Index and 10 Million Records
&lt;/h2&gt;

&lt;p&gt;First, let's create an &lt;code&gt;employees&lt;/code&gt; table with three columns: &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, and &lt;code&gt;salary&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Create the table&lt;/strong&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we defined &lt;code&gt;id&lt;/code&gt; as the &lt;strong&gt;PRIMARY KEY&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In PostgreSQL, whenever we define a primary key, it automatically creates a unique B-tree index on that column.&lt;/p&gt;

&lt;p&gt;In our case, PostgreSQL created an index named &lt;code&gt;employees_pkey&lt;/code&gt; on the &lt;code&gt;id&lt;/code&gt; column.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Insert 10 million records&lt;/strong&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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;employees&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="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salary&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;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'Employee_'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;30000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;70000&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;generate_series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10000000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We now have a table containing 10 million employee records.&lt;/p&gt;

&lt;p&gt;Let's understand how PostgreSQL uses indexes when we execute different queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Indexes Through Queries
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Query 1: Fetching a Record Using an Indexed Column
&lt;/h3&gt;

&lt;p&gt;Let's find the employee whose &lt;code&gt;id&lt;/code&gt; is &lt;code&gt;20&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;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="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&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;p&gt;Before looking at the output, let's understand what &lt;code&gt;EXPLAIN&lt;/code&gt;, &lt;code&gt;ANALYZE&lt;/code&gt;, and &lt;code&gt;BUFFERS&lt;/code&gt; mean.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EXPLAIN:&lt;/strong&gt; Shows how PostgreSQL plans to execute a query without actually running it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ANALYZE:&lt;/strong&gt; Executes the query and shows what actually happened, including the number of rows returned and execution time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BUFFERS:&lt;/strong&gt; Shows how many data block accesses PostgreSQL made during planning and execution, including blocks already available in memory and blocks that had to be read into memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By combining these options, we can understand how PostgreSQL executes our query and how it accesses data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output (First Execution):&lt;/strong&gt;&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%2Ftlvrbmvial0vcy5wn3o1.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%2Ftlvrbmvial0vcy5wn3o1.png" alt=" " width="800" height="171"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, let's understand each part of the output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Index Only Scan&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Index Only Scan using employees_pkey on employees
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;PostgreSQL used the &lt;code&gt;employees_pkey&lt;/code&gt; index, which was automatically created when we defined &lt;code&gt;id&lt;/code&gt; as the primary key.&lt;/li&gt;
&lt;li&gt;We're searching by &lt;code&gt;id&lt;/code&gt; and selecting only &lt;code&gt;id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Since &lt;code&gt;id&lt;/code&gt; is already stored in the index, PostgreSQL can retrieve the required value directly from the index.&lt;/li&gt;
&lt;li&gt;It doesn't need to fetch the full employee record from the table, provided PostgreSQL can confirm that the row is visible without checking the heap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is called an &lt;strong&gt;Index Only Scan&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Cost&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cost=0.43..4.45
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0.43&lt;/code&gt;: Estimated startup cost before PostgreSQL can begin returning rows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;4.45&lt;/code&gt;: Estimated total cost of executing this operation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These values are PostgreSQL's internal cost estimates, not actual execution times in milliseconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Rows and Width&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rows=1 width=4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rows=1&lt;/code&gt;: PostgreSQL estimates that one row will be returned.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;width=4&lt;/code&gt;: The estimated average size of each returned row is 4 bytes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We're selecting only &lt;code&gt;id&lt;/code&gt;, which is an integer occupying 4 bytes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Actual Time, Rows, and Loops&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;actual time=0.058..0.059 rows=1 loops=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0.058&lt;/code&gt;: Time taken until the first row was returned.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0.059&lt;/code&gt;: Time taken until this scan operation completed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rows=1&lt;/code&gt;: One row was actually returned.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;loops=1&lt;/code&gt;: This operation executed once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The actual times are measured in milliseconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Index Condition&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Index Cond: (id = 20)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells us that PostgreSQL used the condition &lt;code&gt;id = 20&lt;/code&gt; to search the index.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Heap Fetches&lt;/strong&gt;&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;PostgreSQL stores actual table records in heap pages.&lt;/p&gt;

&lt;p&gt;An Index Only Scan can avoid visiting heap pages when all the required values are available in the index and PostgreSQL can confirm row visibility using its visibility map.&lt;/p&gt;

&lt;p&gt;Here, &lt;code&gt;Heap Fetches: 0&lt;/code&gt; means PostgreSQL didn't need to visit any heap pages to check row visibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Buffers&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Buffers: shared read=4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL stores frequently accessed data pages in memory, in an area called &lt;strong&gt;shared buffers&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;shared read=4&lt;/code&gt;: PostgreSQL had to read 4 blocks into its shared buffer cache.&lt;/li&gt;
&lt;li&gt;Each PostgreSQL block is typically 8 KB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This doesn't necessarily mean 4 physical disk reads, because the operating system may already have those pages cached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Planning Time and Execution Time&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Planning:
  Buffers: shared hit=41 read=20
Planning Time: 0.616 ms
Execution Time: 0.097 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before executing a query, PostgreSQL first decides how to execute it.&lt;/p&gt;

&lt;p&gt;For example, it checks what indexes are available, estimates how many rows might match, and decides which execution plan would be more efficient.&lt;/p&gt;

&lt;p&gt;During this process, PostgreSQL may need to access database pages containing metadata and statistics.&lt;/p&gt;

&lt;p&gt;That's why we see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Planning:
  Buffers: shared hit=41 read=20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;shared hit=41&lt;/code&gt;: During planning, PostgreSQL made 41 buffer accesses that found pages already available in shared buffers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;shared read=20&lt;/code&gt;: During planning, PostgreSQL made 20 block reads into shared buffers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Planning Time: 0.616 ms&lt;/code&gt;: Time taken to prepare the execution plan.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Execution Time: 0.097 ms&lt;/code&gt;: Time taken to execute the plan and retrieve the result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, PostgreSQL first decides &lt;strong&gt;how to fetch the data (planning)&lt;/strong&gt; and then actually &lt;strong&gt;fetches the data (execution)&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Happens When We Run the Same Query Again?
&lt;/h3&gt;

&lt;p&gt;I executed the exact same query one more time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Output (Second Execution):&lt;/strong&gt;&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%2F2kpx4fyu7vxzprn9l5l3.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%2F2kpx4fyu7vxzprn9l5l3.png" alt=" " width="800" height="149"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice the difference:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;First execution&lt;/th&gt;
&lt;th&gt;Second execution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Execution shared reads&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution shared hits&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Planning time&lt;/td&gt;
&lt;td&gt;0.616 ms&lt;/td&gt;
&lt;td&gt;0.059 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution time&lt;/td&gt;
&lt;td&gt;0.097 ms&lt;/td&gt;
&lt;td&gt;0.027 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In the first execution, PostgreSQL had to read 4 blocks into shared buffers.&lt;/p&gt;

&lt;p&gt;But during the second execution, those blocks were already available in PostgreSQL's memory.&lt;/p&gt;

&lt;p&gt;That's why we see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Buffers: shared hit=4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;shared hit&lt;/strong&gt; means PostgreSQL found the required block in its shared buffer cache without needing to read it into that cache again.&lt;/p&gt;

&lt;p&gt;The second execution was faster in our experiment, although execution times can also vary for other reasons.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing Without PostgreSQL's Existing Cache
&lt;/h3&gt;

&lt;p&gt;If you want to observe how a query behaves when its pages are not already cached in PostgreSQL's shared buffers, you can restart PostgreSQL on your local test machine.&lt;/p&gt;

&lt;p&gt;On Ubuntu:&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="nb"&gt;sudo &lt;/span&gt;systemctl restart postgresql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then execute:&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="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&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;p&gt;Restarting PostgreSQL clears its shared buffer cache, but it does &lt;strong&gt;not&lt;/strong&gt; clear the operating system's page cache. Some pages may also be loaded again before your query runs.&lt;/p&gt;

&lt;p&gt;So this is useful for observing PostgreSQL's buffer behavior, but it doesn't guarantee a completely cold disk-cache experiment.&lt;/p&gt;

&lt;p&gt;Only restart PostgreSQL for this purpose on a local test environment, not on a production or shared database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 2: Fetching a Column That Is Not in the Index
&lt;/h3&gt;

&lt;p&gt;In the previous query, we selected &lt;code&gt;id&lt;/code&gt;, which was already available in the primary key index.&lt;/p&gt;

&lt;p&gt;Now, let's try fetching &lt;code&gt;name&lt;/code&gt; using the same 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;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="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&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%2Fo5brngykgfz5psmnwziq.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%2Fo5brngykgfz5psmnwziq.png" alt=" " width="797" height="135"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, PostgreSQL used an &lt;strong&gt;Index Scan&lt;/strong&gt; instead of an Index Only Scan.&lt;/p&gt;

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

&lt;p&gt;Our &lt;code&gt;employees_pkey&lt;/code&gt; index contains &lt;code&gt;id&lt;/code&gt;, but not &lt;code&gt;name&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So PostgreSQL:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Searches the index to find &lt;code&gt;id = 500&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Gets the reference to the corresponding heap row.&lt;/li&gt;
&lt;li&gt;Visits the heap to fetch the &lt;code&gt;name&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is called an &lt;strong&gt;Index Scan&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Notice the estimated cost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cost=0.43..8.45 rows=1 width=16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The total estimated cost is &lt;code&gt;8.45&lt;/code&gt;, compared with &lt;code&gt;4.45&lt;/code&gt; in Query 1, because PostgreSQL also needs to fetch the row from the heap.&lt;/p&gt;

&lt;p&gt;The estimated &lt;code&gt;width&lt;/code&gt; is now &lt;code&gt;16&lt;/code&gt; bytes instead of &lt;code&gt;4&lt;/code&gt;, because we're selecting a text column rather than an integer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Buffers: shared hit=2 read=2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During execution, PostgreSQL made 2 shared-buffer hits and 2 shared-buffer reads, covering the index search and heap access.&lt;/p&gt;

&lt;p&gt;The execution took &lt;code&gt;0.093 ms&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the difference is simple: &lt;strong&gt;an Index Only Scan can return data from the index without visiting the heap when visibility permits, whereas this Index Scan uses the index to find the row and then visits the heap to retrieve &lt;code&gt;name&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 3: Searching Using a Column Without an Index
&lt;/h3&gt;

&lt;p&gt;Until now, we've been searching using &lt;code&gt;id&lt;/code&gt;, which has an index.&lt;/p&gt;

&lt;p&gt;But what happens if we search using a column that doesn't have an index?&lt;/p&gt;

&lt;p&gt;Let's try searching using &lt;code&gt;name&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;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="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Employee_500'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&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%2F2pvhn8v13mx4ip5slvz7.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%2F2pvhn8v13mx4ip5slvz7.png" alt=" " width="800" height="213"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here, PostgreSQL used a &lt;strong&gt;Parallel Sequential Scan&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let's understand what happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Parallel Sequential Scan&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Since we haven't created an index on &lt;code&gt;name&lt;/code&gt;, PostgreSQL cannot use an index on that column to locate the required employee.&lt;/p&gt;

&lt;p&gt;Instead, it scans the table and checks the &lt;code&gt;name&lt;/code&gt; of each row against our condition.&lt;/p&gt;

&lt;p&gt;But rather than using a single process, PostgreSQL decided to scan the table in parallel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Workers Planned and Launched&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Workers Planned: 2
Workers Launched: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL planned to use 2 additional worker processes and successfully launched both.&lt;/p&gt;

&lt;p&gt;Along with the main process, 3 processes participated in scanning the table.&lt;/p&gt;

&lt;p&gt;Each process works on different portions of the table, allowing the scan to finish faster than a single-process scan might.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Filter and Rows Removed&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Filter: (name = 'Employee_500'::text)
Rows Removed by Filter: 3333333
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL checks each row against the condition &lt;code&gt;name = 'Employee_500'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Rows that don't match are removed by the filter.&lt;/p&gt;

&lt;p&gt;Here, &lt;code&gt;Rows Removed by Filter: 3333333&lt;/code&gt; is the average number of rows rejected per execution of the parallel scan, rounded for display.&lt;/p&gt;

&lt;p&gt;Since &lt;code&gt;loops=3&lt;/code&gt;, approximately 10 million rows were checked across all three participants.&lt;/p&gt;

&lt;p&gt;Even though we wanted only one employee, PostgreSQL had to examine the entire table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Gather&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Gather  (cost=1000.00..125620.26 rows=1 width=4)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Gather&lt;/code&gt; operation collects the rows produced by the parallel scan participants and passes them to the main query process.&lt;/p&gt;

&lt;p&gt;In our case, only one row matched the condition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Buffer Usage&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Buffers: shared hit=1 read=72545
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During execution, PostgreSQL made:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 shared-buffer hit.&lt;/li&gt;
&lt;li&gt;72,545 shared-buffer reads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the usual 8 KB page size, those reads represent approximately &lt;strong&gt;567 MB of block-read traffic&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Notice that the &lt;code&gt;Gather&lt;/code&gt; and &lt;code&gt;Parallel Seq Scan&lt;/code&gt; show the same buffer totals. We shouldn't add them together, because the parent node's statistics include the work of its child node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Execution Time&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution Time: 374.165 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query took &lt;code&gt;374.165 ms&lt;/code&gt; to execute.&lt;/p&gt;

&lt;p&gt;Compare that with Query 2, where we searched using the indexed &lt;code&gt;id&lt;/code&gt; column and the execution took &lt;code&gt;0.093 ms&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The difference is significant in these particular runs.&lt;/p&gt;

&lt;p&gt;With the index, PostgreSQL could locate the row directly. Without an index on &lt;code&gt;name&lt;/code&gt;, it scanned all 10 million records to find one matching employee.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 4: Creating an Index on the Name Column and Comparing Query Performance
&lt;/h3&gt;

&lt;p&gt;In the previous query, PostgreSQL scanned all 10 million records because there was no index on &lt;code&gt;name&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now, let's create an index on that column.&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;employees_name_idx&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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 creates a B-tree index on &lt;code&gt;name&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let's execute the query again.&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="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Employee_500'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&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%2Ferhrje93yv7br3v4gz0g.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%2Ferhrje93yv7br3v4gz0g.png" alt=" " width="800" height="131"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This time, PostgreSQL used an &lt;strong&gt;Index Scan&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Index Scan&lt;/strong&gt;&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 employees_name_idx on employees
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL searches the new index for &lt;code&gt;Employee_500&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since we're selecting &lt;code&gt;id&lt;/code&gt;, which isn't stored in this index, PostgreSQL then visits the heap to fetch it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Buffer Usage&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Buffers: shared hit=1 read=4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During execution, PostgreSQL made just 5 shared-buffer accesses, compared with 72,546 in the previous query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Execution Time&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution Time: 0.050 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's compare the results before and after creating the index.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Without index&lt;/th&gt;
&lt;th&gt;With index&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scan type&lt;/td&gt;
&lt;td&gt;Parallel Seq Scan&lt;/td&gt;
&lt;td&gt;Index Scan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared hits&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared reads&lt;/td&gt;
&lt;td&gt;72,545&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution time&lt;/td&gt;
&lt;td&gt;374.165 ms&lt;/td&gt;
&lt;td&gt;0.050 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The execution time decreased from &lt;code&gt;374.165 ms&lt;/code&gt; to &lt;code&gt;0.050 ms&lt;/code&gt; — approximately &lt;strong&gt;7,483 times faster&lt;/strong&gt; in these two runs.&lt;/p&gt;

&lt;p&gt;Of course, caching and other execution conditions can also affect these timings, so we shouldn't expect exactly the same improvement every time.&lt;/p&gt;

&lt;p&gt;But the important difference is &lt;strong&gt;how PostgreSQL searches for the record&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Earlier, PostgreSQL had to scan the entire table to find &lt;code&gt;Employee_500&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After creating the index, it searched the B-tree index for that particular name and used the matching entry to fetch the row from the heap.&lt;/p&gt;

&lt;p&gt;So even if the execution time varies due to caching, PostgreSQL no longer needs to scan all 10 million table records just to find that employee.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 5: Sorting Records Without an Index
&lt;/h3&gt;

&lt;p&gt;So far, we've seen how indexes help PostgreSQL search for records. But indexes can also help when sorting data.&lt;/p&gt;

&lt;p&gt;Let's find the top 100 employees with the highest salaries.&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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&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;salary&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;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&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%2Fr59dpviy9woj2sxb0zvg.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%2Fr59dpviy9woj2sxb0zvg.png" alt=" " width="799" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; When reading an &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; execution plan, it's usually easier to start from the &lt;strong&gt;bottom and move toward the top&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The lower-level operations produce data, which is then passed to their parent operations.&lt;/p&gt;

&lt;p&gt;In our query, the data flows through:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parallel Seq Scan&lt;/strong&gt; – Reads the records from the table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort&lt;/strong&gt; – Finds the highest salaries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gather Merge&lt;/strong&gt; – Combines the sorted results from the parallel workers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limit&lt;/strong&gt; – Returns only the top 100 records.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a useful way to understand execution plans, although some operations can run at the same time.&lt;/p&gt;

&lt;p&gt;Now, let's understand each operation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Parallel Sequential Scan&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Since there is no index on &lt;code&gt;salary&lt;/code&gt;, PostgreSQL scans the table to find the highest salaries.&lt;/p&gt;

&lt;p&gt;Even though we requested only 100 records, PostgreSQL still needs to examine all 10 million records to determine which employees have the highest salaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Sort&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sort Key: salary DESC
Sort Method: top-N heapsort  Memory: 31kB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL needs to arrange the results by salary in descending order.&lt;/p&gt;

&lt;p&gt;But notice that it used &lt;strong&gt;top-N heapsort&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Since we requested only the top 100 records, PostgreSQL doesn't need to fully sort all 10 million records.&lt;/p&gt;

&lt;p&gt;Instead, each parallel scan participant keeps track of its best 100 candidates while processing the rows.&lt;/p&gt;

&lt;p&gt;This reduces the memory required for sorting, but the table still has to be scanned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Gather Merge&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Gather Merge
Workers Planned: 2
Workers Launched: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parallel participants produce sorted results.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Gather Merge&lt;/code&gt; combines these results while maintaining the salary order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Limit&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Finally, PostgreSQL returns only the first 100 records from the sorted results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Buffer Usage and Execution Time&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Buffers: shared hit=273 read=72385
Execution Time: 575.346 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During execution, PostgreSQL made 273 shared-buffer hits and 72,385 shared-buffer reads.&lt;/p&gt;

&lt;p&gt;The query took &lt;strong&gt;575.346 ms&lt;/strong&gt; to execute.&lt;/p&gt;

&lt;p&gt;Even though we requested just 100 records, PostgreSQL had to examine all 10 million records to identify the highest salaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 6: Sorting Records After Creating an Index on Salary
&lt;/h3&gt;

&lt;p&gt;In the previous query, PostgreSQL scanned all 10 million records and performed sorting to find the top 100 highest salaries.&lt;/p&gt;

&lt;p&gt;Now, let's create an index on &lt;code&gt;salary&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;employees_salary_idx&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;salary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a B-tree index on the &lt;code&gt;salary&lt;/code&gt; column, where the index entries are maintained in sorted order.&lt;/p&gt;

&lt;p&gt;Let's run the same query again.&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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&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;salary&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;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&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%2F0qt8m293jn4eyr911nz5.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%2F0qt8m293jn4eyr911nz5.png" alt=" " width="800" height="171"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice that PostgreSQL used an &lt;strong&gt;Index Scan Backward&lt;/strong&gt; instead of a Parallel Sequential Scan followed by sorting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Index Scan Backward&lt;/strong&gt;&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 Backward using employees_salary_idx on employees
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A B-tree index keeps its entries in sorted order.&lt;/p&gt;

&lt;p&gt;Since our query uses &lt;code&gt;ORDER BY salary DESC&lt;/code&gt;, PostgreSQL can scan the index backward, starting from the highest salary values.&lt;/p&gt;

&lt;p&gt;It doesn't need to sort all 10 million records separately.&lt;/p&gt;

&lt;p&gt;And because we're also selecting &lt;code&gt;id&lt;/code&gt;, which isn't stored in the &lt;code&gt;salary&lt;/code&gt; index, PostgreSQL visits the heap to fetch it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Limit&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Limit (cost=0.43..5.17 rows=100 width=8)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where the &lt;code&gt;LIMIT 100&lt;/code&gt; makes a big difference.&lt;/p&gt;

&lt;p&gt;PostgreSQL starts reading the index from the highest salary and stops after returning 100 records.&lt;/p&gt;

&lt;p&gt;Even though the index contains entries for 10 million records, PostgreSQL doesn't need to scan all of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Buffer Usage&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Buffers: shared read=103
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL made only 103 shared-buffer reads during execution.&lt;/p&gt;

&lt;p&gt;Compare that with the previous query, which required 72,658 buffer accesses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Execution Time&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution Time: 1.123 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's compare both executions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Without salary index&lt;/th&gt;
&lt;th&gt;With salary index&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Execution plan&lt;/td&gt;
&lt;td&gt;Parallel Seq Scan + Sort&lt;/td&gt;
&lt;td&gt;Index Scan Backward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared hits&lt;/td&gt;
&lt;td&gt;273&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared reads&lt;/td&gt;
&lt;td&gt;72,385&lt;/td&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution time&lt;/td&gt;
&lt;td&gt;575.346 ms&lt;/td&gt;
&lt;td&gt;1.123 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The execution time decreased from &lt;code&gt;575.346 ms&lt;/code&gt; to &lt;code&gt;1.123 ms&lt;/code&gt;, approximately &lt;strong&gt;512 times faster&lt;/strong&gt; in these runs.&lt;/p&gt;

&lt;p&gt;Although caching can affect execution times, the important difference is that PostgreSQL no longer needs to scan the entire table and sort the results.&lt;/p&gt;

&lt;p&gt;It can simply scan the index backward, retrieve the highest salary records, and stop after finding 100 rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 7: Why Doesn't PostgreSQL Always Use an Index?
&lt;/h3&gt;

&lt;p&gt;Until now, we've seen how indexes improved query performance.&lt;/p&gt;

&lt;p&gt;But does PostgreSQL always use an index whenever one is available?&lt;/p&gt;

&lt;p&gt;Let's try searching for employees whose salary is greater than &lt;code&gt;40000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Remember, we already created an index on the &lt;code&gt;salary&lt;/code&gt; column.&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="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;40000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&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%2Flv8kmvl9z3oci8dg9jzm.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%2Flv8kmvl9z3oci8dg9jzm.png" alt=" " width="800" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Interesting! PostgreSQL chose a &lt;strong&gt;Sequential Scan&lt;/strong&gt;, even though an index exists on &lt;code&gt;salary&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;1. Sequential Scan&lt;/strong&gt;&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 employees
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our condition is:&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;WHERE&lt;/span&gt; &lt;span class="n"&gt;salary&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;40000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matches a large number of employees.&lt;/p&gt;

&lt;p&gt;In our case, PostgreSQL found &lt;strong&gt;8,572,502 matching records out of 10 million&lt;/strong&gt;, which is approximately 85.7% of the table.&lt;/p&gt;

&lt;p&gt;If PostgreSQL used the &lt;code&gt;salary&lt;/code&gt; index, it would need to find all matching index entries and then visit the heap to fetch &lt;code&gt;id&lt;/code&gt;, since &lt;code&gt;id&lt;/code&gt; isn't stored in that index.&lt;/p&gt;

&lt;p&gt;For so many matching records, accessing the heap through the index could require more work than simply scanning the table.&lt;/p&gt;

&lt;p&gt;So PostgreSQL estimated that a Sequential Scan would be cheaper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Rows Removed by Filter&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Filter: (salary &amp;gt; 40000)
Rows Removed by Filter: 1427498
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL scanned all 10 million records.&lt;/p&gt;

&lt;p&gt;Of those:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;8,572,502 records matched the condition.&lt;/li&gt;
&lt;li&gt;1,427,498 records were removed by the filter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Buffer Usage&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Buffers: shared hit=16247 read=56299
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL accessed 72,546 shared-buffer blocks while scanning the table.&lt;/p&gt;

&lt;p&gt;Some pages were already cached, while others had to be read into shared buffers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Execution Time&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Execution Time: 1123.251 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query took approximately &lt;strong&gt;1.12 seconds&lt;/strong&gt; to execute and return over 8.5 million rows to the query executor.&lt;/p&gt;

&lt;p&gt;Here, the important thing is that &lt;strong&gt;having an index doesn't mean PostgreSQL will always use it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;PostgreSQL's query planner estimates the cost of different execution plans and chooses the one it expects to be cheaper.&lt;/p&gt;

&lt;p&gt;When a query returns a small number of records, an index can be very useful. But when most of the table matches, a sequential scan may be more efficient.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;In this article, we focused on understanding indexing through practical queries and execution plans.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Part 2&lt;/strong&gt;, we'll explore different types of index scans, when PostgreSQL chooses each one, and how we can design indexes more efficiently.&lt;/p&gt;

&lt;p&gt;That's it for now! 😊&lt;/p&gt;

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

</description>
      <category>computerscience</category>
      <category>database</category>
      <category>indexing</category>
      <category>performance</category>
    </item>
    <item>
      <title>Database Storage Concepts – How Tables Are Stored on Disk</title>
      <dc:creator>Anusha Renangi</dc:creator>
      <pubDate>Wed, 14 Oct 2026 17:51:48 +0000</pubDate>
      <link>https://dev.to/anusha_renangi_05a4e41d35/database-storage-concepts-how-tables-are-stored-on-disk-387h</link>
      <guid>https://dev.to/anusha_renangi_05a4e41d35/database-storage-concepts-how-tables-are-stored-on-disk-387h</guid>
      <description>&lt;p&gt;In my previous article, I discussed database transactions and ACID properties.&lt;/p&gt;

&lt;p&gt;Now, let's understand how databases actually store data.&lt;/p&gt;

&lt;p&gt;Whenever we create a table and insert records, we see the data in rows and columns. But how is this data stored on disk? Does the database read one row at a time? How does it know where a particular row is stored?&lt;/p&gt;

&lt;p&gt;Let's learn the following topics in this article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tables&lt;/li&gt;
&lt;li&gt;Row ID&lt;/li&gt;
&lt;li&gt;Pages&lt;/li&gt;
&lt;li&gt;Disk I/O&lt;/li&gt;
&lt;li&gt;Heap&lt;/li&gt;
&lt;li&gt;Indexes&lt;/li&gt;
&lt;li&gt;Row-oriented and column-oriented storage&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Tables
&lt;/h2&gt;

&lt;p&gt;We know that a table contains rows and columns.&lt;/p&gt;

&lt;p&gt;For example, consider an employee table.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;emp_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;salary&lt;/th&gt;
&lt;th&gt;language&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Anu&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td&gt;English&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Sam&lt;/td&gt;
&lt;td&gt;20000&lt;/td&gt;
&lt;td&gt;Telugu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;td&gt;30000&lt;/td&gt;
&lt;td&gt;Hindi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Ravi&lt;/td&gt;
&lt;td&gt;40000&lt;/td&gt;
&lt;td&gt;English&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Harry&lt;/td&gt;
&lt;td&gt;50000&lt;/td&gt;
&lt;td&gt;Telugu&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is how we logically see the data.&lt;/p&gt;

&lt;p&gt;But internally, databases store these records in pages on disk.&lt;/p&gt;

&lt;p&gt;Before understanding pages, let's see what a Row ID is.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Row ID
&lt;/h2&gt;

&lt;p&gt;A Row ID is an identifier used internally by a database to locate or reference a row.&lt;/p&gt;

&lt;p&gt;It is different from the primary key we define in a table.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;emp_id&lt;/code&gt; is the primary key in our employee table. But databases may also maintain their own internal row identifiers.&lt;/p&gt;

&lt;p&gt;In PostgreSQL, we have a system column called &lt;code&gt;ctid&lt;/code&gt;, which tells us the physical location of a particular row version.&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;ctid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result might look like:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ctid&lt;/th&gt;
&lt;th&gt;emp_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;(0,1)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Anu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(0,2)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Sam&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(0,3)&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(1,1)&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Ravi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(1,2)&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Harry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here, &lt;code&gt;(0,1)&lt;/code&gt; means the row is at page 0, item position 1.&lt;/p&gt;

&lt;p&gt;Unlike a primary key, &lt;code&gt;ctid&lt;/code&gt; is not permanent. It can change when a row is updated or moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Pages
&lt;/h2&gt;

&lt;p&gt;A page is a fixed-size block used to store and read data.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;PostgreSQL generally uses 8 KB pages.&lt;/li&gt;
&lt;li&gt;MySQL InnoDB generally uses 16 KB pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's assume each page can hold three employee records.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Page 0&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;emp_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;salary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Anu&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Sam&lt;/td&gt;
&lt;td&gt;20000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;td&gt;30000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Page 1&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;emp_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;salary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Ravi&lt;/td&gt;
&lt;td&gt;40000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Harry&lt;/td&gt;
&lt;td&gt;50000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In reality, the number of rows a page can hold depends on their sizes and the space used for page metadata.&lt;/p&gt;

&lt;p&gt;One important thing is that &lt;strong&gt;the database generally reads data in pages, not as individual rows directly from disk.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Suppose we need the record with &lt;code&gt;emp_id = 2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If Page 0 needs to be fetched from disk, the database reads that page into memory. Since the page also contains records 1 and 3, those records are available in the same page.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. I/O (Input/Output)
&lt;/h2&gt;

&lt;p&gt;I/O refers to reading data from or writing data to storage.&lt;/p&gt;

&lt;p&gt;When a database needs a page that is not already in memory, it has to read it from disk.&lt;/p&gt;

&lt;p&gt;Disk access is generally slower than accessing data from RAM.&lt;/p&gt;

&lt;p&gt;So databases try to minimize unnecessary I/O operations.&lt;/p&gt;

&lt;p&gt;Let's take an example.&lt;/p&gt;

&lt;p&gt;Suppose we have 1,000 records, and each page holds 10 records.&lt;/p&gt;

&lt;p&gt;Then we need approximately 100 pages to store those records, ignoring some storage overhead.&lt;/p&gt;

&lt;p&gt;Now suppose we want to find a particular record but don't know which page contains it.&lt;/p&gt;

&lt;p&gt;The database may need to read multiple pages until it finds the required record.&lt;/p&gt;

&lt;p&gt;Reading 100 pages is more expensive than reading just one page, especially if those pages are not already in memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does every query read data from disk?
&lt;/h3&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;Databases also use memory to cache pages.&lt;/p&gt;

&lt;p&gt;For example, PostgreSQL uses a memory area called &lt;strong&gt;Shared Buffers&lt;/strong&gt;. The operating system also maintains a page cache.&lt;/p&gt;

&lt;p&gt;When a page is read for the first time, it may need to be fetched from disk.&lt;/p&gt;

&lt;p&gt;If the same page is needed again and is still cached in memory, the database may be able to access it without another physical disk read.&lt;/p&gt;

&lt;p&gt;This reduces I/O and improves performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Heap
&lt;/h2&gt;

&lt;p&gt;In PostgreSQL, ordinary tables use a storage structure called a &lt;strong&gt;Heap&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A heap is a collection of pages containing the actual table rows.&lt;/p&gt;

&lt;p&gt;For example, the employee table might have:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heap – Employees&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Page&lt;/th&gt;
&lt;th&gt;Records&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Page 0&lt;/td&gt;
&lt;td&gt;Anu, Sam, John&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page 1&lt;/td&gt;
&lt;td&gt;Ravi, Harry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page 2&lt;/td&gt;
&lt;td&gt;Priya, Kiran, Rahul&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page 3&lt;/td&gt;
&lt;td&gt;Sneha, Arjun, Meena&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The actual data of the table is stored in these pages.&lt;/p&gt;

&lt;p&gt;The important point about a PostgreSQL heap is that &lt;strong&gt;rows are not maintained in any particular order based on a column&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For example, even if we insert records in the order 1, 2, 3, 4, 5, we cannot assume that they will always remain physically stored in that order.&lt;/p&gt;

&lt;p&gt;Now suppose we want to find an employee whose ID is 100.&lt;/p&gt;

&lt;p&gt;Without knowing the location of that record, PostgreSQL may need to examine many heap pages to find it.&lt;/p&gt;

&lt;p&gt;For a table with millions of records, this can require a lot of data to be read.&lt;/p&gt;

&lt;p&gt;Databases maintain separate structures called indexes, which help locate records more efficiently.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Index
&lt;/h2&gt;

&lt;p&gt;An index is a separate data structure that helps the database find the location of required records.&lt;/p&gt;

&lt;p&gt;For example, suppose we create an index on &lt;code&gt;emp_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The index keeps employee ID values along with references to their corresponding rows.&lt;/p&gt;

&lt;p&gt;A simplified example:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Employee ID&lt;/th&gt;
&lt;th&gt;Row Location&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;(0,1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;(0,2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;(0,3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;(1,1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;(1,2)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here, the row location tells us which heap page and item contain the record.&lt;/p&gt;

&lt;p&gt;Instead of checking every heap page, the database can use the index to find the required location.&lt;/p&gt;

&lt;p&gt;Indexes are also stored in pages on disk. So reading an index can also involve I/O.&lt;/p&gt;

&lt;p&gt;One commonly used index data structure is a B-tree.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An index doesn't replace the heap in PostgreSQL.&lt;/strong&gt; It is maintained separately and contains information that helps locate the actual table records.&lt;/p&gt;

&lt;p&gt;I'll cover indexes, B-trees, and how queries use indexes in more detail in a separate article.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Row-Oriented Database
&lt;/h2&gt;

&lt;p&gt;In a row-oriented database, the values belonging to the same row are stored together.&lt;/p&gt;

&lt;p&gt;PostgreSQL and MySQL InnoDB are examples of row-oriented databases.&lt;/p&gt;

&lt;p&gt;Let's take our employee table.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;th&gt;salary&lt;/th&gt;
&lt;th&gt;language&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Anu&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td&gt;English&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Sam&lt;/td&gt;
&lt;td&gt;20000&lt;/td&gt;
&lt;td&gt;Telugu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;td&gt;30000&lt;/td&gt;
&lt;td&gt;Hindi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Ravi&lt;/td&gt;
&lt;td&gt;40000&lt;/td&gt;
&lt;td&gt;English&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In row-oriented storage, the data is organized approximately like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Page 0&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[1, Anu, 10000, English]
[2, Sam, 20000, Telugu]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Page 1&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[3, John, 30000, Hindi]
[4, Ravi, 40000, English]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The values of each employee are stored together.&lt;/p&gt;

&lt;p&gt;Let's consider three queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 1: Find an employee's name
&lt;/h3&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;name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database needs to find the row where &lt;code&gt;id = 2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Suppose there is no index on the ID column.&lt;/p&gt;

&lt;p&gt;The database starts scanning the table pages.&lt;/p&gt;

&lt;p&gt;First, it reads Page 0 and checks the employee IDs in that page.&lt;/p&gt;

&lt;p&gt;It checks the first row, where &lt;code&gt;id = 1&lt;/code&gt;. Since this is not the required ID, it moves to the next row.&lt;/p&gt;

&lt;p&gt;The next row contains &lt;code&gt;id = 2&lt;/code&gt;, so the database finds the required employee.&lt;/p&gt;

&lt;p&gt;Since the row contains all the columns together, it can directly get the name &lt;code&gt;Sam&lt;/code&gt; from that row.&lt;/p&gt;

&lt;p&gt;If the record were not in Page 0, the database would continue checking the next pages until it found the matching row.&lt;/p&gt;

&lt;p&gt;If an index is available and the database chooses to use it, it can locate the required row without scanning every table page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 2: Fetch all employee details
&lt;/h3&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="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we need all columns of one employee.&lt;/p&gt;

&lt;p&gt;Row-oriented storage is useful because all the values belonging to that row are stored together.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 3: Calculate the total salary
&lt;/h3&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;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;salary&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;employees&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we only need the salary column.&lt;/p&gt;

&lt;p&gt;But in row-oriented storage, salary values are stored along with the other columns of each employee.&lt;/p&gt;

&lt;p&gt;So, when reading the table pages, the database may also bring unnecessary data into memory.&lt;/p&gt;

&lt;p&gt;For a large table containing many columns, this can result in more data being read than we actually need.&lt;/p&gt;

&lt;p&gt;This is one limitation of row-oriented storage for analytical queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Column-Oriented Database
&lt;/h2&gt;

&lt;p&gt;In a column-oriented database, values belonging to the same column are stored together.&lt;/p&gt;

&lt;p&gt;Let's consider the same employee table.&lt;/p&gt;

&lt;p&gt;Instead of storing complete rows together, the data is organized by columns.&lt;/p&gt;

&lt;p&gt;Each column can be divided into multiple blocks depending on the amount of data.&lt;/p&gt;

&lt;p&gt;For example, let's assume each block holds two values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ID column&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Block 0: [1:1001, 2:1002]
Block 1: [3:1003, 4:1004]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Name column&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Block 0: [Anu:1001, Sam:1002]
Block 1: [John:1003, Ravi:1004]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Salary column&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Block 0: [10000:1001, 20000:1002]
Block 1: [30000:1003, 40000:1004]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Language column&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Block 0: [English:1001, Telugu:1002]
Block 1: [Hindi:1003, English:1004]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;1001&lt;/code&gt;, &lt;code&gt;1002&lt;/code&gt;, etc., represent row IDs.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;Sam:1002&lt;/code&gt; and &lt;code&gt;20000:1002&lt;/code&gt; belong to the same employee.&lt;/p&gt;

&lt;p&gt;This is a simplified representation. Actual column-oriented databases may use row positions or other methods instead of storing an individual row ID with every value.&lt;/p&gt;

&lt;p&gt;Column-oriented databases store values in column blocks, often in compressed form.&lt;/p&gt;

&lt;p&gt;Let's use the same three queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 1: Find an employee's name
&lt;/h3&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;name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database needs the ID and Name columns.&lt;/p&gt;

&lt;p&gt;Assuming there is no index or other information to locate the value directly, it searches the ID blocks until it finds &lt;code&gt;2:1002&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now it knows the row ID is &lt;code&gt;1002&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, it accesses the Name column and finds &lt;code&gt;Sam:1002&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the result is &lt;code&gt;Sam&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here, the database accesses two different columns to get the result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 2: Fetch all employee details
&lt;/h3&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="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we need every column.&lt;/p&gt;

&lt;p&gt;First, the database finds &lt;code&gt;2:1002&lt;/code&gt; in the ID column.&lt;/p&gt;

&lt;p&gt;Then it retrieves the corresponding values from the other columns using row ID &lt;code&gt;1002&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: &lt;code&gt;Sam:1002&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Salary: &lt;code&gt;20000:1002&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Language: &lt;code&gt;Telugu:1002&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since values are stored column-wise, the database may need to read multiple column blocks to get the complete employee record.&lt;/p&gt;

&lt;p&gt;This can involve more work compared with retrieving a complete row from row-oriented storage.&lt;/p&gt;

&lt;p&gt;Similarly, updates or deletes involving several columns may require more work because the values are stored separately. Some column-oriented databases handle this using update or delete markers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query 3: Calculate the total salary
&lt;/h3&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;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;salary&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;employees&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we only need the Salary column.&lt;/p&gt;

&lt;p&gt;The database can read only the Salary blocks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Block 0: [10000, 20000]
Block 1: [30000, 40000]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no need to read the Name, ID, or Language columns.&lt;/p&gt;

&lt;p&gt;This reduces the amount of data read.&lt;/p&gt;

&lt;p&gt;Column-oriented storage is very useful for queries involving aggregations over large numbers of records.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compression
&lt;/h3&gt;

&lt;p&gt;Column-oriented databases can also achieve better compression.&lt;/p&gt;

&lt;p&gt;For example, suppose we have 1 million employee records, and many employees have the same language or department.&lt;/p&gt;

&lt;p&gt;Since the values of a particular column are stored together, repeated values can be compressed efficiently.&lt;/p&gt;

&lt;p&gt;In row-oriented storage, different data types and values are stored together as part of each row, so this type of compression may not be as effective.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Row-Oriented vs Column-Oriented Storage
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Row-Oriented&lt;/th&gt;
&lt;th&gt;Column-Oriented&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stores data row by row&lt;/td&gt;
&lt;td&gt;Stores data column by column&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Good for reading complete records&lt;/td&gt;
&lt;td&gt;Good for reading selected columns across many records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generally efficient for frequent inserts and updates&lt;/td&gt;
&lt;td&gt;Small updates can be more expensive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commonly used for OLTP&lt;/td&gt;
&lt;td&gt;Commonly used for OLAP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregations over a few columns may read unnecessary data&lt;/td&gt;
&lt;td&gt;Aggregations can read only the required columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compression is generally less effective&lt;/td&gt;
&lt;td&gt;Often achieves better compression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL, MySQL InnoDB&lt;/td&gt;
&lt;td&gt;Amazon Redshift, Google BigQuery, ClickHouse&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;OLTP (Online Transaction Processing)&lt;/strong&gt; usually involves frequent inserts, updates, deletes, and fetching individual records.&lt;/p&gt;

&lt;p&gt;For example, creating an order, updating customer details, or retrieving a particular employee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OLAP (Online Analytical Processing)&lt;/strong&gt; usually involves queries that process many records, such as calculating total sales, generating reports, or finding average salaries.&lt;/p&gt;

&lt;p&gt;Both types of databases can execute these operations. The difference is in how their storage is organized and the workloads they are generally optimized for.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>computerscience</category>
      <category>database</category>
    </item>
    <item>
      <title>Software Design Principles Every Developer Should Know: SOLID, DRY, KISS, and YAGNI</title>
      <dc:creator>Anusha Renangi</dc:creator>
      <pubDate>Mon, 12 Oct 2026 14:18:41 +0000</pubDate>
      <link>https://dev.to/anusha_renangi_05a4e41d35/software-design-principles-every-developer-should-know-solid-dry-kiss-and-yagni-l62</link>
      <guid>https://dev.to/anusha_renangi_05a4e41d35/software-design-principles-every-developer-should-know-solid-dry-kiss-and-yagni-l62</guid>
      <description>&lt;p&gt;When we write code, our first goal is usually to make it work.&lt;/p&gt;

&lt;p&gt;But what happens when requirements change?&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%2F0834xx2v3ms1ey6ga8g6.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%2F0834xx2v3ms1ey6ga8g6.png" alt=" " width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Initially, our code might work perfectly. But as the application grows, even a small change can become difficult if the code is poorly structured.&lt;/p&gt;

&lt;p&gt;So, writing code that works is important, but writing code that is easy to understand and maintain is equally important.&lt;/p&gt;

&lt;p&gt;While learning software design, I came across a few principles that help us write better code.&lt;/p&gt;

&lt;p&gt;I have already written about the &lt;strong&gt;SOLID principles&lt;/strong&gt; with real-life examples.&lt;/p&gt;

&lt;p&gt;You can read that article here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/anusha_renangi_05a4e41d35/solid-principles-explained-through-real-life-examples-1mad"&gt;SOLID Principles Explained Through Real-Life Examples&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, I'll cover three more principles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;DRY: Don't Repeat Yourself&lt;/li&gt;
&lt;li&gt;KISS: Keep It Simple, Stupid&lt;/li&gt;
&lt;li&gt;YAGNI: You Aren't Gonna Need It&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. DRY Principle (Don't Repeat Yourself)
&lt;/h2&gt;

&lt;p&gt;DRY stands for &lt;strong&gt;Don't Repeat Yourself&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The idea is to avoid writing the same logic in multiple places.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem: Repeating the Same Logic
&lt;/h3&gt;

&lt;p&gt;Suppose we are developing an online shopping application and need to calculate the final price after applying a discount.&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="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt;
&lt;span class="n"&gt;final_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, suppose we need the same calculation on another page. We write the same logic again.&lt;/p&gt;

&lt;p&gt;If the discount changes, we must update both places. If we forget one, the application might calculate different prices.&lt;/p&gt;

&lt;h3&gt;
  
  
  Applying DRY
&lt;/h3&gt;

&lt;p&gt;Instead, we can create a reusable function.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_final_price&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;


&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;calculate_final_price&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;calculate_final_price&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the calculation is written in one place and can be reused wherever needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Cases of DRY
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Reusing Common Logic
&lt;/h3&gt;

&lt;p&gt;Suppose we need to check whether a person is an adult in different parts of an application.&lt;/p&gt;

&lt;p&gt;Instead of writing the same condition repeatedly, we can create a reusable function.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_adult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;


&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;is_adult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;is_adult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we can call &lt;code&gt;is_adult()&lt;/code&gt; wherever we need to check a person's age.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Sharing Common Functionality Through Inheritance
&lt;/h3&gt;

&lt;p&gt;Suppose we have two types of employees: Developers and Testers.&lt;/p&gt;

&lt;p&gt;Both have a name and need to display it. However, developers write code, while testers test applications.&lt;/p&gt;

&lt;p&gt;Instead of repeating the common functionality, we can put it in a parent class.&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Employee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Employee: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Developer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Employee&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is writing code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Tester&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Employee&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_application&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is testing the application&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;developer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Developer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Anu&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;tester&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Tester&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ravi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;developer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;developer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_code&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;tester&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;display_name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;tester&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test_application&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both classes reuse &lt;code&gt;display_name()&lt;/code&gt; from the &lt;code&gt;Employee&lt;/code&gt; class, while each class has its own functionality.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Using Constants for Repeated Values
&lt;/h3&gt;

&lt;p&gt;Suppose we use the same tax rate in multiple calculations.&lt;/p&gt;

&lt;p&gt;Instead of hardcoding the value everywhere, we can define a constant.&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="n"&gt;TAX_RATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.18&lt;/span&gt;

&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;span class="n"&gt;tax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;TAX_RATE&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tax&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the tax rate changes, we can update it in one place.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Creating Reusable Modules
&lt;/h3&gt;

&lt;p&gt;Suppose multiple Python files need to calculate the area of a circle.&lt;/p&gt;

&lt;p&gt;Instead of writing the same function in every file, we can create a separate module.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;math_utils.py&lt;/code&gt;:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;circle_area&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt; &lt;span class="o"&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;In another file:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;math_utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;circle_area&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;circle_area&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we can reuse the same function in different files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits of DRY
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Less duplicated code:&lt;/strong&gt; We avoid repeating the same logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easier maintenance:&lt;/strong&gt; We can update shared logic in one place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent behavior:&lt;/strong&gt; The same logic works across different parts of the application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easier testing:&lt;/strong&gt; We can test reusable functions independently.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Over-abstraction:&lt;/strong&gt; Combining unrelated logic can make code difficult to understand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unwanted dependencies:&lt;/strong&gt; Changes to shared code can affect multiple parts of the application.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We should avoid repeating the same logic, but we don't need to combine code just because it looks similar.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. KISS Principle (Keep It Simple, Stupid)
&lt;/h2&gt;

&lt;p&gt;KISS stands for &lt;strong&gt;Keep It Simple, Stupid&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The idea is to avoid making code more complicated than necessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;Suppose we want to check whether a number is even.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_even&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But do we really need an &lt;code&gt;if-else&lt;/code&gt; statement here?&lt;/p&gt;

&lt;h3&gt;
  
  
  Applying KISS
&lt;/h3&gt;

&lt;p&gt;We can simplify it:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_even&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both functions produce the same result, but the second one is simpler and easier to read.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to Apply KISS
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Break large problems into smaller functions.&lt;/li&gt;
&lt;li&gt;Use clear variable and function names.&lt;/li&gt;
&lt;li&gt;Avoid unnecessary complexity.&lt;/li&gt;
&lt;li&gt;Choose straightforward solutions when they meet the requirements.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Benefits of KISS
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Easier to read and understand.&lt;/li&gt;
&lt;li&gt;Easier to debug and maintain.&lt;/li&gt;
&lt;li&gt;Less unnecessary complexity.&lt;/li&gt;
&lt;li&gt;Faster code reviews.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons
&lt;/h3&gt;

&lt;p&gt;Oversimplifying code can make it difficult to handle important requirements or edge cases.&lt;/p&gt;

&lt;p&gt;Remember, &lt;strong&gt;shorter code is not always simpler code.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. YAGNI Principle (You Aren't Gonna Need It)
&lt;/h2&gt;

&lt;p&gt;YAGNI stands for &lt;strong&gt;You Aren't Gonna Need It&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The idea is to build what is required now instead of implementing features just because we might need them in the future.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem: Building Unnecessary Features
&lt;/h3&gt;

&lt;p&gt;Suppose we are developing a payment system that currently supports only credit card payments.&lt;/p&gt;

&lt;p&gt;While developing it, we think:&lt;/p&gt;

&lt;p&gt;"What if we need PayPal or cryptocurrency payments in the future?"&lt;/p&gt;

&lt;p&gt;So, we start implementing support for those payment methods even though nobody has requested them.&lt;/p&gt;

&lt;p&gt;This takes extra time and makes the code more complicated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Applying YAGNI
&lt;/h3&gt;

&lt;p&gt;For now, we should implement only the required credit card payment functionality.&lt;/p&gt;

&lt;p&gt;If the requirements change later, we can add the necessary features then.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to Apply YAGNI
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Understand the current requirements.&lt;/li&gt;
&lt;li&gt;Focus on features that are actually needed.&lt;/li&gt;
&lt;li&gt;Avoid implementing hypothetical features.&lt;/li&gt;
&lt;li&gt;Add functionality when there is a real requirement for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Benefits of YAGNI
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Faster development.&lt;/li&gt;
&lt;li&gt;Less unnecessary code.&lt;/li&gt;
&lt;li&gt;Reduced development effort.&lt;/li&gt;
&lt;li&gt;Better focus on current requirements.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Future requirements may require refactoring.&lt;/li&gt;
&lt;li&gt;New features may take longer if the existing design needs changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;YAGNI doesn't mean ignoring future maintainability. It means avoiding unnecessary implementation before it is needed.&lt;/p&gt;

&lt;p&gt;The goal is to write code that solves the current problem and is easy to understand and maintain.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>yagni</category>
      <category>dry</category>
      <category>solidprinciples</category>
    </item>
    <item>
      <title>SOLID Principles Explained Through Real-Life Examples</title>
      <dc:creator>Anusha Renangi</dc:creator>
      <pubDate>Mon, 12 Oct 2026 13:03:38 +0000</pubDate>
      <link>https://dev.to/anusha_renangi_05a4e41d35/solid-principles-explained-through-real-life-examples-1mad</link>
      <guid>https://dev.to/anusha_renangi_05a4e41d35/solid-principles-explained-through-real-life-examples-1mad</guid>
      <description>&lt;p&gt;When we write code, making it work is only one part of the job. As requirements change and applications grow, we also need to think about how easy our code is to understand, maintain, and extend.&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%2F7mdav2r60fiy0g84lr75.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%2F7mdav2r60fiy0g84lr75.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While learning software design, I came across the SOLID principles. I didn't want to just memorize their definitions, so I tried to understand each principle through real-life situations and then relate it to code.&lt;/p&gt;

&lt;p&gt;In this article, I'll share those examples to explain the five SOLID principles in simple terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  S: Single Responsibility Principle
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A real-life example
&lt;/h3&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%2Fbdxhdn7715e3fl5rlg5n.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%2Fbdxhdn7715e3fl5rlg5n.png" alt=" " width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The owner of my hostel also managed a curry point.&lt;/p&gt;

&lt;p&gt;Whenever something went wrong in the hostel, he had to spend time fixing it. During that time, managing the curry point became difficult. The same thing happened when there was a problem at the curry point.&lt;/p&gt;

&lt;p&gt;Both businesses depended on the same person, so a problem in one affected the other.&lt;/p&gt;

&lt;p&gt;Now, imagine having separate people to manage the hostel and the curry point. A problem in the hostel could be handled by the hostel manager while the curry point continued its work.&lt;/p&gt;

&lt;p&gt;We can apply a similar idea when designing software.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a responsibility?
&lt;/h3&gt;

&lt;p&gt;A responsibility is the job a class is meant to handle.&lt;/p&gt;

&lt;p&gt;Let's say the owner builds software to manage both businesses.&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BusinessManager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;book_hostel_room&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;schedule_hostel_repairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;take_food_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_food_bill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This class handles two different responsibilities:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hostel management&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Booking rooms&lt;/li&gt;
&lt;li&gt;Scheduling repairs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Curry point management&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Taking food orders&lt;/li&gt;
&lt;li&gt;Calculating food bills&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Although all these methods belong to the same &lt;code&gt;BusinessManager&lt;/code&gt; class, they handle two different areas of work.&lt;/p&gt;

&lt;p&gt;If hostel booking rules change, we should not need to modify the code that handles food orders. Similarly, a change in food billing should not affect hostel management.&lt;/p&gt;

&lt;p&gt;We can separate these responsibilities into two classes.&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HostelManager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;book_room&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;schedule_repairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CurryPointManager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;take_food_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_food_bill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, hostel-related changes can be handled in &lt;code&gt;HostelManager&lt;/code&gt;, and curry point-related changes can be handled in &lt;code&gt;CurryPointManager&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Notice that we did not create a separate class for every method. Room booking and repair scheduling can stay together because both are part of hostel management.&lt;/p&gt;

&lt;h3&gt;
  
  
  The principle
&lt;/h3&gt;

&lt;p&gt;The Single Responsibility Principle (SRP) states:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A class should have only one reason to change.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This does not mean a class should have only one method. It means that the methods in a class should belong to one clear responsibility.&lt;/p&gt;

&lt;p&gt;When a class handles different responsibilities that change for different reasons, we should consider separating them.&lt;/p&gt;

&lt;p&gt;The goal is to make changes in one area without unnecessarily affecting other areas.&lt;/p&gt;

&lt;h1&gt;
  
  
  O: Open/Closed Principle
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgdb5oz6t8bi95unj5qnz.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%2Fgdb5oz6t8bi95unj5qnz.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was thinking of building an app for my uncle to manage his curry point.&lt;/p&gt;

&lt;p&gt;He sells egg curry and chicken curry. I wanted the app to help him manage the items and their prices.&lt;/p&gt;

&lt;p&gt;Now, suppose he decides to add mutton curry to the menu. I would have to change the code to add its price.&lt;/p&gt;

&lt;p&gt;What if he wants to add another curry later? I would have to make changes again.&lt;/p&gt;

&lt;p&gt;I wanted to write the code in a way that adding a new curry would be easier, without changing the code that already works.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the Open/Closed Principle?
&lt;/h2&gt;

&lt;p&gt;The Open/Closed Principle says:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Software entities should be open for extension, but closed for modification.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In simple words, we should be able to add new features without having to change the existing code every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. My first approach
&lt;/h2&gt;

&lt;p&gt;I can write a function to calculate the price of each curry.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_price&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;egg_curry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chicken_curry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works for the current menu.&lt;/p&gt;

&lt;p&gt;But when my uncle wants to add mutton curry, I need to modify the same function.&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="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mutton_curry&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time he adds a new curry, I have to add another condition to this function.&lt;/p&gt;

&lt;p&gt;Can we avoid changing this function whenever a new curry is added?&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Create a common class for curries
&lt;/h2&gt;

&lt;p&gt;Let's create a class called &lt;code&gt;Curry&lt;/code&gt; that defines a common method for calculating prices.&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;abc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abstractmethod&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Curry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@abstractmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_price&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Curry&lt;/code&gt; class defines a rule: every curry must have a &lt;code&gt;calculate_price()&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;It doesn't calculate the actual price. Each curry will provide its own implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Create classes for each curry
&lt;/h2&gt;

&lt;p&gt;Now, let's create classes for egg curry and chicken curry.&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EggCurry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Curry&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_price&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChickenCurry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Curry&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_price&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each class implements the same method, but returns a different price.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Write the billing function
&lt;/h2&gt;

&lt;p&gt;Now, we can write a common function to calculate the price.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_bill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;curry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Curry&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;curry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;calculate_price&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's use it:&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;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;generate_bill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;EggCurry&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;      &lt;span class="c1"&gt;# 80
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;generate_bill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChickenCurry&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;  &lt;span class="c1"&gt;# 150
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that &lt;code&gt;generate_bill()&lt;/code&gt; doesn't check whether the curry is egg curry or chicken curry. It simply calls &lt;code&gt;calculate_price()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Python calls the method implemented by the actual object. This is called &lt;strong&gt;polymorphism&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Add mutton curry
&lt;/h2&gt;

&lt;p&gt;Now my uncle wants to add mutton curry to the menu.&lt;/p&gt;

&lt;p&gt;I just need to create another class.&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MuttonCurry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Curry&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_price&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can use the same billing function:&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;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;generate_bill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MuttonCurry&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;  &lt;span class="c1"&gt;# 250
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When my uncle wants to add mutton curry, I don't have to touch the &lt;code&gt;generate_bill()&lt;/code&gt; function. I just add the &lt;code&gt;MuttonCurry&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;The same function works for egg curry, chicken curry, and mutton curry. That's what I wanted from the design.&lt;/p&gt;

&lt;h1&gt;
  
  
  L: Liskov Substitution Principle
&lt;/h1&gt;

&lt;p&gt;LSP says that &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Objects of a child class should be replaceable with objects of their parent class without breaking the application.&lt;/strong&gt;&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%2F0ep4xwa2g3fvwqb77zwq.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%2F0ep4xwa2g3fvwqb77zwq.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's understand this with an example.&lt;/p&gt;

&lt;h2&gt;
  
  
  An Authentication Example
&lt;/h2&gt;

&lt;p&gt;Imagine we are building an application that supports different authentication methods, such as passwords, biometrics, and two-factor authentication.&lt;/p&gt;

&lt;p&gt;We create a parent class called &lt;code&gt;AuthenticationProvider&lt;/code&gt;.&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthenticationProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        Returns True if authentication succeeds,
        or False if the credentials are invalid.
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;NotImplementedError&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The contract is simple. Every authentication provider must return &lt;code&gt;True&lt;/code&gt; or &lt;code&gt;False&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Password Authentication
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PasswordAuthProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthenticationProvider&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pass&lt;/span&gt;&lt;span class="sh"&gt;"&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;If the credentials are valid, it returns &lt;code&gt;True&lt;/code&gt;. Otherwise, it returns &lt;code&gt;False&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This follows the parent's contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  Biometric Authentication
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BiometricAuthProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthenticationProvider&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mobile_hardware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify_fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fingerprint_token&lt;/span&gt;&lt;span class="sh"&gt;"&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;This also returns &lt;code&gt;True&lt;/code&gt; or &lt;code&gt;False&lt;/code&gt;, so it follows the same contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two-Factor Authentication
&lt;/h3&gt;

&lt;p&gt;Now, let's implement authentication using SMS verification.&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TwoFactorSmsAuthProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthenticationProvider&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sms_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_pin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;phone_number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;MultiFactorChallengeRequired&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This implementation sends an SMS PIN but then raises an exception instead of returning a Boolean.&lt;/p&gt;

&lt;p&gt;The problem is not that two-factor authentication requires an extra step. The problem is that this implementation breaks the contract promised by the parent class.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Does This Break the Application?
&lt;/h2&gt;

&lt;p&gt;Consider the client code:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_user_login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;is_logged_in&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_logged_in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;display_dashboard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;display_error_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid credentials&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This client expects &lt;code&gt;authenticate()&lt;/code&gt; to return a Boolean.&lt;/p&gt;

&lt;p&gt;It works with password and biometric authentication. But when we pass &lt;code&gt;TwoFactorSmsAuthProvider&lt;/code&gt;, the unexpected exception interrupts the flow.&lt;/p&gt;

&lt;p&gt;The child class cannot safely replace the parent class in this client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That is an LSP violation.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Can We Fix It?
&lt;/h2&gt;

&lt;p&gt;We need to design a contract that supports all the expected authentication outcomes.&lt;/p&gt;

&lt;p&gt;For example, authentication can return one of these results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SUCCESS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INVALID_CREDENTIALS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MFA_REQUIRED&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two-factor provider can send the PIN and return &lt;code&gt;MFA_REQUIRED&lt;/code&gt; instead of throwing an unexpected exception.&lt;/p&gt;

&lt;p&gt;The client can handle these common outcomes:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_user_login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SUCCESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;display_dashboard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INVALID_CREDENTIALS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;display_error_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid credentials&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MFA_REQUIRED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;display_pin_screen&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, every provider follows the same contract. The client handles the defined outcomes without needing to know which concrete provider it is using.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;if/elif&lt;/code&gt; statements are not the problem here. They handle common outcomes defined by the contract. The problem would be forcing the client to add separate logic for each authentication provider.&lt;/p&gt;

&lt;h1&gt;
  
  
  I: Interface Segregation Principle
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F46m9b4pu1n9td67e34sz.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%2F46m9b4pu1n9td67e34sz.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Interface Segregation Principle (ISP) says that a class should not be forced to implement methods it does not need.&lt;/p&gt;

&lt;p&gt;Instead of creating one large interface with many methods, we should split it into smaller interfaces based on what different classes actually need.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: One Large Interface
&lt;/h2&gt;

&lt;p&gt;Imagine we are building a user management system. We have two types of users: &lt;strong&gt;Customers&lt;/strong&gt; and &lt;strong&gt;Administrators&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Customers can log in and change their passwords. Administrators can do these things too, but they can also delete user accounts.&lt;/p&gt;

&lt;p&gt;Now, suppose we put all these actions into a single &lt;code&gt;IUser&lt;/code&gt; interface.&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;delete_user_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we create a &lt;code&gt;Customer&lt;/code&gt; class, it must implement all three methods, even though customers are not allowed to delete user accounts.&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IUser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Customer logged in.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Password changed.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;delete_user_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;delete_user_account()&lt;/code&gt; method does nothing for a customer. We have included it only because the interface forces us to implement it.&lt;/p&gt;

&lt;p&gt;This is the problem ISP helps us avoid.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Split the Interface
&lt;/h2&gt;

&lt;p&gt;Instead of putting every action into one interface, we can separate the actions based on their responsibilities.&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IStandardUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IAdminUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;delete_user_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, each interface represents a smaller set of actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Customer
&lt;/h3&gt;

&lt;p&gt;A customer needs only standard user actions.&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IStandardUser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Customer logged in.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Password changed.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Customer&lt;/code&gt; class no longer needs an unnecessary &lt;code&gt;delete_user_account()&lt;/code&gt; method.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Administrator
&lt;/h3&gt;

&lt;p&gt;An administrator can perform both standard user actions and administrative actions. So, the class can implement both interfaces.&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Administrator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IStandardUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IAdminUser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Admin logged in.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Admin password changed.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;delete_user_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Admin deleted an account.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each class now implements only the actions it is expected to support.&lt;/p&gt;

&lt;h1&gt;
  
  
  D: Dependency Inversion Principle
&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1y5enteec7riuqr4rj5i.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%2F1y5enteec7riuqr4rj5i.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Imagine you are working as a backend developer in a software company. You know how to build applications using Django.&lt;/p&gt;

&lt;p&gt;Your company currently uses one technology, but after some time, the team decides to move to a different technology.&lt;/p&gt;

&lt;p&gt;If you know only Django and are unwilling to learn anything new, you may struggle to adapt to the team's changing requirements.&lt;/p&gt;

&lt;p&gt;The same situation can happen in software design.&lt;/p&gt;

&lt;p&gt;Imagine we have an application that sends SMS notifications using Twilio. If our code is tightly connected to Twilio, switching to another provider, such as Vonage, may require us to change the existing code.&lt;/p&gt;

&lt;p&gt;Instead, we can design our code so that it does not depend on one specific SMS provider. Then, we can change the provider without changing the main business logic.&lt;/p&gt;

&lt;p&gt;This is the idea behind the &lt;strong&gt;Dependency Inversion Principle&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Dependency Inversion Principle?
&lt;/h2&gt;

&lt;p&gt;The Dependency Inversion Principle says that high-level modules should not depend directly on low-level modules. Both should depend on abstractions.&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%2Ff7fl89vdtvv1jr7q0gw8.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%2Ff7fl89vdtvv1jr7q0gw8.png" alt=" " width="790" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In simple words, our main business logic should not depend directly on a specific implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Depending Directly on Twilio
&lt;/h2&gt;

&lt;p&gt;Imagine our application sends an SMS whenever a customer places an order.&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TwilioService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_sms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sending SMS through Twilio: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sms_service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TwilioService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;place_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Order placed.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sms_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_sms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Your order has been placed!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;OrderService&lt;/code&gt; creates a &lt;code&gt;TwilioService&lt;/code&gt; object directly.&lt;/p&gt;

&lt;p&gt;If we want to switch to Vonage, we need to change how &lt;code&gt;OrderService&lt;/code&gt; creates and uses the SMS service.&lt;/p&gt;

&lt;p&gt;The order-processing logic is unnecessarily tied to Twilio.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Can We Fix It?
&lt;/h2&gt;

&lt;p&gt;We can define a common interface that describes how an SMS service should work.&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;abc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abstractmethod&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SMSService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@abstractmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_sms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, both providers can implement this interface.&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TwilioService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SMSService&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_sms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sending SMS through Twilio: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VonageService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SMSService&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_sms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Sending SMS through Vonage: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we update &lt;code&gt;OrderService&lt;/code&gt; to receive an SMS service instead of creating a Twilio object itself.&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sms_service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SMSService&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sms_service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sms_service&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;place_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Order placed.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sms_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_sms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Your order has been placed!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we can choose the provider when creating the service.&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="n"&gt;order_service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TwilioService&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or we can use Vonage instead.&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="n"&gt;order_service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;VonageService&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We do not need to change the &lt;code&gt;OrderService&lt;/code&gt; class when switching providers.&lt;/p&gt;

&lt;p&gt;⭐ Hope this helps! 😊&lt;/p&gt;

&lt;p&gt;I was really confused about the difference between LSP and ISP. I went through several Reddit discussions, Stack Overflow answers, and other resources to understand them better.&lt;/p&gt;

&lt;p&gt;If you're struggling with the same confusion, let's discuss it in the comments! &lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>lld</category>
      <category>designpatterns</category>
      <category>solidprinciples</category>
    </item>
    <item>
      <title>Database Transactions - ACID Properties</title>
      <dc:creator>Anusha Renangi</dc:creator>
      <pubDate>Fri, 09 Oct 2026 20:39:40 +0000</pubDate>
      <link>https://dev.to/anusha_renangi_05a4e41d35/database-transactions-acid-properties-2d7p</link>
      <guid>https://dev.to/anusha_renangi_05a4e41d35/database-transactions-acid-properties-2d7p</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Yesterday, I went to a restaurant with my friends. We ordered some food, finished eating, and asked the waiter for the bill.&lt;/p&gt;

&lt;p&gt;While the waiter was preparing the bill, I started thinking about how the billing system works behind the scenes.&lt;/p&gt;

&lt;p&gt;When we place an order, the system has to do multiple things, like saving the order details, calculating the bill, and updating the payment status.&lt;/p&gt;

&lt;p&gt;But what if something goes wrong in the middle? What if the system crashes before completing all these operations?&lt;/p&gt;

&lt;p&gt;This is where database transactions come into the picture.&lt;/p&gt;

&lt;p&gt;Recently, I was learning about database transactions, ACID properties, and isolation levels. In this article, I'll share what I understood, along with some simple examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a Database Transaction?
&lt;/h2&gt;

&lt;p&gt;A database transaction is a group of one or more database operations that are treated as a single unit of work.&lt;/p&gt;

&lt;p&gt;Let's take our restaurant example.&lt;/p&gt;

&lt;p&gt;When a customer places an order, the system might need to perform these operations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an order.&lt;/li&gt;
&lt;li&gt;Save the ordered items.&lt;/li&gt;
&lt;li&gt;Update the available stock.&lt;/li&gt;
&lt;li&gt;Calculate and save the total bill.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All these operations are related. If one of them fails, we don't want the database to be left with incomplete information.&lt;/p&gt;

&lt;p&gt;So, instead of executing them as separate independent operations, we can group them into a single transaction.&lt;/p&gt;

&lt;p&gt;A transaction generally involves three important commands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BEGIN&lt;/strong&gt; — Starts a transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;COMMIT&lt;/strong&gt; — Saves the changes made by the transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ROLLBACK&lt;/strong&gt; — Undoes the uncommitted changes if something goes wrong.&lt;/li&gt;
&lt;/ul&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;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&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;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;order_items&lt;/span&gt; &lt;span class="p"&gt;(&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;item_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&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;Here, all three database operations are part of one transaction.&lt;/p&gt;

&lt;p&gt;If everything succeeds, we commit the transaction.&lt;/p&gt;

&lt;p&gt;If an operation fails, we can roll back the transaction so that the database doesn't keep only part of the changes.&lt;/p&gt;

&lt;p&gt;But what happens if the database itself crashes while executing a transaction?&lt;/p&gt;

&lt;p&gt;A database can crash for several reasons, such as power failures, hardware problems, operating system failures, or software bugs.&lt;/p&gt;

&lt;p&gt;Even in these situations, the database needs a way to recover without leaving incomplete transactions behind.&lt;/p&gt;

&lt;p&gt;To handle these kinds of problems and maintain reliable data, database transactions follow four important properties, commonly called &lt;strong&gt;ACID properties&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  ACID Properties
&lt;/h2&gt;

&lt;p&gt;ACID stands for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A — Atomicity&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C — Consistency&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I — Isolation&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;D — Durability&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each property solves a different problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Atomicity&lt;/strong&gt; ensures that either all operations in a transaction happen or none of them happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistency&lt;/strong&gt; ensures that a transaction preserves the rules and constraints defined in the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Isolation&lt;/strong&gt; controls how multiple transactions running at the same time interact with each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Durability&lt;/strong&gt; ensures that once a transaction is committed, its changes survive a crash, under the database's durability guarantees.&lt;/p&gt;

&lt;p&gt;Let's understand each property with examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  Atomicity
&lt;/h2&gt;

&lt;p&gt;Atomicity means that either all the operations in a transaction should succeed, or none of them should happen.&lt;/p&gt;

&lt;p&gt;Let's take our restaurant example.&lt;/p&gt;

&lt;p&gt;Suppose a customer orders 2 pizzas, and each pizza costs ₹200.&lt;/p&gt;

&lt;p&gt;When the waiter confirms the order, the system needs to perform two operations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an order with a total amount of ₹400.&lt;/li&gt;
&lt;li&gt;Reduce the available pizza stock by 2.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, let's say the order is created successfully, but the system crashes before updating the stock.&lt;/p&gt;

&lt;p&gt;In this case, the order exists in the database, but the stock has not been updated. This leaves the data in an incorrect state.&lt;/p&gt;

&lt;p&gt;To avoid this, we can execute both operations inside a transaction.&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;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&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;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&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;If both operations succeed, the transaction is committed.&lt;/p&gt;

&lt;p&gt;But if something goes wrong before the transaction is committed, the database rolls back the changes.&lt;/p&gt;

&lt;p&gt;So, even if the order was inserted successfully, it won't remain in the database if the transaction fails.&lt;/p&gt;

&lt;p&gt;This is what atomicity guarantees: &lt;strong&gt;All or nothing.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How does the database maintain atomicity?
&lt;/h3&gt;

&lt;p&gt;Databases use transaction logs and recovery mechanisms to handle failures.&lt;/p&gt;

&lt;p&gt;For example, PostgreSQL uses &lt;strong&gt;Write-Ahead Logging (WAL)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Before modified data pages are written to their final location on disk, PostgreSQL records the changes in WAL.&lt;/p&gt;

&lt;p&gt;If the database crashes, it uses WAL and transaction status information during recovery to ensure that committed transactions are recovered and incomplete transactions do not become visible as committed changes.&lt;/p&gt;

&lt;p&gt;We'll understand WAL in more detail when we discuss durability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consistency
&lt;/h2&gt;

&lt;p&gt;Consistency means that a transaction should maintain the rules and constraints defined in the database.&lt;/p&gt;

&lt;p&gt;While learning about consistency, I came across two different concepts: &lt;strong&gt;Data Consistency&lt;/strong&gt; and &lt;strong&gt;Read Consistency&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Data Consistency (Correctness)
&lt;/h3&gt;

&lt;p&gt;Data consistency is about keeping the data correct and making sure it follows the rules defined in the database.&lt;/p&gt;

&lt;p&gt;Let's say we have three tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blogs Table&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;blog_id&lt;/th&gt;
&lt;th&gt;likes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Users Table&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;John&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Edmon&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Pictures Table&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;user_id&lt;/th&gt;
&lt;th&gt;picture_id&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here, &lt;code&gt;user_id&lt;/code&gt; in the Pictures table is a foreign key referencing the Users table.&lt;/p&gt;

&lt;p&gt;Now suppose we try to insert a picture with &lt;code&gt;user_id = 5&lt;/code&gt;, but there is no user with that ID in the Users 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;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;pictures&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;picture_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database rejects this operation because it violates the foreign key constraint.&lt;/p&gt;

&lt;p&gt;This is called &lt;strong&gt;referential integrity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Similarly, databases maintain data consistency using constraints such as primary keys, foreign keys, UNIQUE, NOT NULL, and CHECK.&lt;/p&gt;

&lt;p&gt;Apart from these constraints, we may also have business rules that need to be maintained by our application or database.&lt;/p&gt;

&lt;p&gt;So, the main idea of ACID Consistency is that &lt;strong&gt;a transaction should move the database from one valid state to another valid state without breaking the defined rules.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In simple terms, it focuses on &lt;strong&gt;correctness of data&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Read Consistency (Availability and Freshness)
&lt;/h3&gt;

&lt;p&gt;Now let's look at consistency from another perspective.&lt;/p&gt;

&lt;p&gt;In real applications, we may have multiple copies of the same database, called replicas.&lt;/p&gt;

&lt;p&gt;Why do we need replicas?&lt;/p&gt;

&lt;p&gt;One reason is availability. If one database server goes down, another server may still be able to serve requests. Replicas can also help distribute read traffic.&lt;/p&gt;

&lt;p&gt;Let's say we have one primary database and two replicas.&lt;/p&gt;

&lt;p&gt;Initially, all three databases contain the same value.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Primary Database → 90&lt;/li&gt;
&lt;li&gt;Replica 1 → 90&lt;/li&gt;
&lt;li&gt;Replica 2 → 90&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now suppose a transaction updates the value from &lt;code&gt;90&lt;/code&gt; to &lt;code&gt;100&lt;/code&gt; in the primary database and commits.&lt;/p&gt;

&lt;p&gt;But replication may take some time.&lt;/p&gt;

&lt;p&gt;At that moment, the data might look like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Primary Database → 100&lt;/li&gt;
&lt;li&gt;Replica 1 → 90&lt;/li&gt;
&lt;li&gt;Replica 2 → 90&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now imagine two users trying to read that value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;T1:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;User 1 reads from the primary database and gets &lt;code&gt;100&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;T2:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;User 2 reads from Replica 1 and gets &lt;code&gt;90&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Both users are reading the same record, but they are getting different values.&lt;/p&gt;

&lt;p&gt;This happens because the replicas haven't received the latest update yet.&lt;/p&gt;

&lt;p&gt;After some time, replication catches up.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Primary Database → 100&lt;/li&gt;
&lt;li&gt;Replica 1 → 100&lt;/li&gt;
&lt;li&gt;Replica 2 → 100&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now all three databases have the same value.&lt;/p&gt;

&lt;p&gt;This is an example of &lt;strong&gt;Eventual Consistency&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It means different replicas may temporarily have different values, but eventually they become consistent if updates stop and replication completes successfully.&lt;/p&gt;

&lt;h3&gt;
  
  
  Availability vs. Consistency
&lt;/h3&gt;

&lt;p&gt;Here, we have an interesting trade-off.&lt;/p&gt;

&lt;p&gt;Suppose a replica hasn't received the latest update yet.&lt;/p&gt;

&lt;p&gt;We have two possible approaches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Allow the read&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We can allow the user to read from that replica, even though it has older data.&lt;/p&gt;

&lt;p&gt;The user gets a response immediately, but the value might be stale.&lt;/p&gt;

&lt;p&gt;This favors availability, although it doesn't guarantee that the user gets the latest data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2: Wait for the latest data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We can wait until the replica catches up, or route the request to a database that has the required version.&lt;/p&gt;

&lt;p&gt;The user gets fresher data, but the request may take longer or become unavailable if the required data cannot be reached.&lt;/p&gt;

&lt;p&gt;So, depending on the system's requirements, we may need to make trade-offs between availability and the consistency of reads, especially during network failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the Difference?
&lt;/h3&gt;

&lt;p&gt;The main thing I understood is that these two concepts focus on different problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ACID Consistency → Correctness&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It makes sure the database follows its defined rules and constraints. A transaction should not leave the data in an invalid state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read Consistency → Freshness of Data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It defines what values a reader is allowed to see, especially when data is replicated across multiple servers.&lt;/p&gt;

&lt;p&gt;Availability is related to read consistency because allowing reads from replicas with slightly older data can help a system continue serving requests.&lt;/p&gt;

&lt;p&gt;So, &lt;strong&gt;ACID consistency is mainly about keeping data correct, while consistency in distributed reads is about how up-to-date and consistent the data needs to be across different replicas.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolation
&lt;/h2&gt;

&lt;p&gt;Until now, we have seen how transactions maintain atomicity and consistency.&lt;/p&gt;

&lt;p&gt;But what happens when multiple transactions run at the same time?&lt;/p&gt;

&lt;p&gt;Let's take our restaurant example.&lt;/p&gt;

&lt;p&gt;Suppose two waiters are taking orders from different tables. Both are using the same billing system.&lt;/p&gt;

&lt;p&gt;Now imagine only one pizza is left in stock, and both waiters are trying to place an order for it.&lt;/p&gt;

&lt;p&gt;If both transactions read the available quantity at the same time, they might both think the pizza is available.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;Isolation&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;Isolation controls how multiple transactions running at the same time interact with each other.&lt;/p&gt;

&lt;p&gt;Ideally, we want the result of concurrent transactions to be the same as some valid order of executing them one after another. But running every transaction one after another can affect performance.&lt;/p&gt;

&lt;p&gt;So databases provide different isolation levels.&lt;/p&gt;

&lt;p&gt;Before understanding them, let's look at four common problems that happen when transactions execute concurrently.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dirty Read&lt;/li&gt;
&lt;li&gt;Non-Repeatable Read&lt;/li&gt;
&lt;li&gt;Phantom Read&lt;/li&gt;
&lt;li&gt;Lost Update&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1. Dirty Read
&lt;/h3&gt;

&lt;p&gt;A dirty read happens when a transaction reads a change made by another transaction that hasn't committed yet.&lt;/p&gt;

&lt;p&gt;Let's take a sales table.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;pid&lt;/th&gt;
&lt;th&gt;qnt&lt;/th&gt;
&lt;th&gt;price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;P1&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P2&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Suppose two transactions are running.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;TX1&lt;/th&gt;
&lt;th&gt;TX2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BEGIN;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SELECT pid, qnt * price FROM sales;&lt;/code&gt; → P1: 50, P2: 80&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BEGIN;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T4&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UPDATE sales SET qnt = qnt + 5 WHERE pid = 'P1';&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T5&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SELECT SUM(qnt * price) FROM sales;&lt;/code&gt; → &lt;strong&gt;155&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T6&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ROLLBACK;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T7&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COMMIT;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At T5, TX1 reads the updated value from TX2 even though TX2 hasn't committed yet.&lt;/p&gt;

&lt;p&gt;Later, TX2 rolls back its changes, but TX1 has already used that uncommitted data.&lt;/p&gt;

&lt;p&gt;This is called a &lt;strong&gt;Dirty Read&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL note:&lt;/strong&gt; This example represents a database that allows dirty reads. PostgreSQL doesn't allow them, even with Read Uncommitted.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Non-Repeatable Read
&lt;/h3&gt;

&lt;p&gt;A non-repeatable read happens when a transaction reads the same record twice and gets different values because another transaction committed an update in between.&lt;/p&gt;

&lt;p&gt;Let's use the same Sales table.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;TX1&lt;/th&gt;
&lt;th&gt;TX2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BEGIN;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SELECT pid, qnt * price FROM sales;&lt;/code&gt; → P1: 50, P2: 80&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BEGIN;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T4&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UPDATE sales SET qnt = qnt + 5 WHERE pid = 'P1';&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T5&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COMMIT;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T6&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SELECT SUM(qnt * price) FROM sales;&lt;/code&gt; → &lt;strong&gt;155&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T7&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COMMIT;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here, TX1 initially reads the sales values.&lt;/p&gt;

&lt;p&gt;Meanwhile, TX2 updates the quantity and commits.&lt;/p&gt;

&lt;p&gt;When TX1 later calculates the total, it sees the updated data.&lt;/p&gt;

&lt;p&gt;So TX1 gets different results within the same transaction.&lt;/p&gt;

&lt;p&gt;This demonstrates the effect of a non-repeatable read. Strictly speaking, the standard definition involves reading the same row twice; here, the second statement is an aggregation over the changed rows.&lt;/p&gt;

&lt;p&gt;This can happen under PostgreSQL's default Read Committed isolation level.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Phantom Read
&lt;/h3&gt;

&lt;p&gt;A phantom read happens when a transaction executes a query and later gets a different set of matching rows because another transaction inserts, deletes, or changes matching records.&lt;/p&gt;

&lt;p&gt;Suppose we have this table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;id&lt;/th&gt;
&lt;th&gt;ch&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;b&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;b&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;TX1&lt;/th&gt;
&lt;th&gt;TX2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BEGIN;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SELECT * FROM test WHERE ch = 'b';&lt;/code&gt; → &lt;strong&gt;2 rows&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BEGIN;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T4&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INSERT INTO test (id, ch) VALUES (5, 'b');&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T5&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COMMIT;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T6&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SELECT * FROM test WHERE ch = 'b';&lt;/code&gt; → &lt;strong&gt;3 rows&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T7&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COMMIT;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Initially, TX1 gets two records.&lt;/p&gt;

&lt;p&gt;Meanwhile, TX2 inserts another record with &lt;code&gt;ch = 'b'&lt;/code&gt; and commits.&lt;/p&gt;

&lt;p&gt;When TX1 executes the same query again, it gets three records.&lt;/p&gt;

&lt;p&gt;The newly appearing record is called a &lt;strong&gt;phantom row&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;PostgreSQL's Repeatable Read prevents phantom reads because the transaction continues using the same snapshot.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Lost Update
&lt;/h3&gt;

&lt;p&gt;A lost update happens when two transactions read the same value and update it independently, causing one transaction's change to be overwritten.&lt;/p&gt;

&lt;p&gt;Suppose P1's current quantity is &lt;code&gt;10&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;TX1 wants to increase it by 10, and TX2 wants to increase it by 5.&lt;/p&gt;

&lt;p&gt;The expected final quantity is &lt;code&gt;25&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;TX1&lt;/th&gt;
&lt;th&gt;TX2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BEGIN;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SELECT qnt FROM sales WHERE pid = 'P1';&lt;/code&gt; → &lt;strong&gt;10&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BEGIN;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T4&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SELECT qnt FROM sales WHERE pid = 'P1';&lt;/code&gt; → &lt;strong&gt;10&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UPDATE sales SET qnt = 20 WHERE pid = 'P1';&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T6&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;UPDATE sales SET qnt = 15 WHERE pid = 'P1';&lt;/code&gt; → Waits for TX1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T7&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COMMIT;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T8&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;TX2's UPDATE completes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T9&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COMMIT;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The final quantity becomes &lt;code&gt;15&lt;/code&gt; instead of &lt;code&gt;25&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;TX2 overwrites the value written by TX1, so TX1's update is lost.&lt;/p&gt;

&lt;p&gt;This is called a &lt;strong&gt;Lost Update&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This can happen when an application calculates a new value using an earlier read and then writes that fixed value back.&lt;/p&gt;

&lt;p&gt;One way to prevent it is by using &lt;code&gt;SELECT FOR UPDATE&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;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;qnt&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'P1'&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;qnt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qnt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'P1'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&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;Here, PostgreSQL locks the selected row. Another transaction trying to update the same row must wait.&lt;/p&gt;

&lt;p&gt;If we simply want to increment a value, we can also do it directly:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;qnt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qnt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'P1'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids calculating the new value using an outdated application-side read.&lt;/p&gt;




&lt;h2&gt;
  
  
  Database Isolation Levels
&lt;/h2&gt;

&lt;p&gt;Now that we have seen these problems, let's understand how databases prevent them.&lt;/p&gt;

&lt;p&gt;SQL defines four common isolation levels:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read Uncommitted&lt;/li&gt;
&lt;li&gt;Read Committed&lt;/li&gt;
&lt;li&gt;Repeatable Read&lt;/li&gt;
&lt;li&gt;Serializable&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  1. Read Uncommitted
&lt;/h3&gt;

&lt;p&gt;This is the weakest standard isolation level.&lt;/p&gt;

&lt;p&gt;A transaction may read changes made by another transaction before they are committed.&lt;/p&gt;

&lt;p&gt;So dirty reads, non-repeatable reads, and phantom reads are possible.&lt;/p&gt;

&lt;p&gt;For example, in our Dirty Read scenario, TX1 could read the updated sales total even though TX2 later rolls back.&lt;/p&gt;

&lt;p&gt;We can request this isolation level using:&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;BEGIN&lt;/span&gt; &lt;span class="n"&gt;TRANSACTION&lt;/span&gt;
&lt;span class="k"&gt;ISOLATION&lt;/span&gt; &lt;span class="k"&gt;LEVEL&lt;/span&gt; &lt;span class="k"&gt;READ&lt;/span&gt; &lt;span class="k"&gt;UNCOMMITTED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, PostgreSQL treats Read Uncommitted as Read Committed.&lt;/p&gt;

&lt;p&gt;So we cannot reproduce dirty reads in PostgreSQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Read Committed
&lt;/h3&gt;

&lt;p&gt;Read Committed prevents dirty reads.&lt;/p&gt;

&lt;p&gt;A transaction only reads committed data.&lt;/p&gt;

&lt;p&gt;However, each statement gets a new snapshot, so the same query may return different results when executed twice.&lt;/p&gt;

&lt;p&gt;Let's take our sales example.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;TX1&lt;/th&gt;
&lt;th&gt;TX2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BEGIN ISOLATION LEVEL READ COMMITTED;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SELECT pid, qnt * price FROM sales;&lt;/code&gt; → P1: 50, P2: 80&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BEGIN;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T4&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UPDATE sales SET qnt = qnt + 5 WHERE pid = 'P1';&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T5&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COMMIT;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T6&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SELECT SUM(qnt * price) FROM sales;&lt;/code&gt; → &lt;strong&gt;155&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T7&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COMMIT;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Even though TX1 hasn't finished, its next query sees the changes committed by TX2.&lt;/p&gt;

&lt;p&gt;This is allowed because Read Committed creates a new snapshot for each statement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read Committed is PostgreSQL's default isolation level.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Repeatable Read
&lt;/h3&gt;

&lt;p&gt;In Repeatable Read, a transaction continues reading from the same snapshot.&lt;/p&gt;

&lt;p&gt;Let's use the same example.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;TX1&lt;/th&gt;
&lt;th&gt;TX2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BEGIN ISOLATION LEVEL REPEATABLE READ;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SELECT pid, qnt * price FROM sales;&lt;/code&gt; → P1: 50, P2: 80&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BEGIN;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T4&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UPDATE sales SET qnt = qnt + 5 WHERE pid = 'P1';&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T5&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COMMIT;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T6&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SELECT SUM(qnt * price) FROM sales;&lt;/code&gt; → &lt;strong&gt;130&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T7&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COMMIT;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice the difference.&lt;/p&gt;

&lt;p&gt;Even though TX2 committed its update, TX1 still calculates the total using its original snapshot.&lt;/p&gt;

&lt;p&gt;This happens because PostgreSQL uses a consistent snapshot established when the transaction first executes a statement that needs one.&lt;/p&gt;

&lt;p&gt;So changes committed by other transactions after that snapshot aren't visible to TX1.&lt;/p&gt;

&lt;p&gt;One interesting thing about PostgreSQL is that &lt;strong&gt;Repeatable Read also prevents phantom reads&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is stronger than the minimum Repeatable Read guarantees described by the SQL standard.&lt;/p&gt;

&lt;p&gt;However, Repeatable Read doesn't prevent every possible concurrency anomaly.&lt;/p&gt;

&lt;p&gt;For example, write skew can still happen.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Serializable
&lt;/h3&gt;

&lt;p&gt;Serializable is the strongest standard isolation level.&lt;/p&gt;

&lt;p&gt;It ensures that concurrent transactions produce a result equivalent to executing those transactions one after another in some order.&lt;/p&gt;

&lt;p&gt;This doesn't mean PostgreSQL actually runs every transaction sequentially.&lt;/p&gt;

&lt;p&gt;Transactions can still run concurrently.&lt;/p&gt;

&lt;p&gt;But PostgreSQL checks for dangerous dependencies that could produce a result that isn't equivalent to any serial execution.&lt;/p&gt;

&lt;p&gt;PostgreSQL uses &lt;strong&gt;Serializable Snapshot Isolation (SSI)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let's take an example.&lt;/p&gt;

&lt;p&gt;Suppose we have two records.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Record&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Our business rule is that at least one record must always have a value of 100.&lt;/p&gt;

&lt;p&gt;Two transactions are running at Serializable isolation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;TX1&lt;/th&gt;
&lt;th&gt;TX2&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BEGIN ISOLATION LEVEL SERIALIZABLE;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T2&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SELECT value FROM test WHERE id IN ('A', 'B');&lt;/code&gt; → &lt;strong&gt;100, 100&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BEGIN ISOLATION LEVEL SERIALIZABLE;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T4&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SELECT value FROM test WHERE id IN ('A', 'B');&lt;/code&gt; → &lt;strong&gt;100, 100&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UPDATE test SET value = 0 WHERE id = 'A';&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T6&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UPDATE test SET value = 0 WHERE id = 'B';&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T7&lt;/td&gt;
&lt;td&gt;&lt;code&gt;COMMIT;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T8&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;COMMIT;&lt;/code&gt; → &lt;strong&gt;Serialization failure&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both transactions initially read A = 100 and B = 100.&lt;/p&gt;

&lt;p&gt;TX1 changes A to 0.&lt;/p&gt;

&lt;p&gt;TX2 changes B to 0.&lt;/p&gt;

&lt;p&gt;If both transactions committed successfully, we would have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A = 0
B = 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This would violate our business rule.&lt;/p&gt;

&lt;p&gt;This kind of anomaly is called &lt;strong&gt;Write Skew&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Under PostgreSQL Repeatable Read, both transactions may successfully commit because they update different rows.&lt;/p&gt;

&lt;p&gt;But PostgreSQL Serializable can detect the dangerous dependency and abort one transaction.&lt;/p&gt;

&lt;p&gt;We may see an error like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: could not serialize access due to
read/write dependencies among transactions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When this happens, the application needs to retry the entire transaction.&lt;/p&gt;

&lt;p&gt;Serializable is useful when we need stronger correctness guarantees, but we should also be prepared to handle transaction retries.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pessimistic and Optimistic Concurrency Control
&lt;/h2&gt;

&lt;p&gt;While learning about isolation, I also came across two ways of handling concurrent updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pessimistic Concurrency Control
&lt;/h3&gt;

&lt;p&gt;Here, we lock the data before changing it.&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;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;qnt&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'P1'&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;sales&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;qnt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qnt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'P1'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&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;The &lt;code&gt;FOR UPDATE&lt;/code&gt; statement locks the selected row.&lt;/p&gt;

&lt;p&gt;Another transaction trying to acquire a conflicting lock must wait until the first transaction releases it.&lt;/p&gt;

&lt;p&gt;This is useful when conflicts are likely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optimistic Concurrency Control
&lt;/h3&gt;

&lt;p&gt;In optimistic concurrency control, we allow transactions to proceed without locking the record exclusively in advance.&lt;/p&gt;

&lt;p&gt;Before applying the update, we check whether another transaction has changed the data.&lt;/p&gt;

&lt;p&gt;If a conflicting change is detected, the operation may fail and need to be retried.&lt;/p&gt;

&lt;p&gt;This approach can be useful when conflicts are less frequent.&lt;/p&gt;

&lt;p&gt;PostgreSQL's Serializable Snapshot Isolation also uses conflict detection, although its implementation is different from application-level version checking.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparison of Isolation Levels
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Isolation Level&lt;/th&gt;
&lt;th&gt;Dirty Read&lt;/th&gt;
&lt;th&gt;Non-Repeatable Read&lt;/th&gt;
&lt;th&gt;Phantom Read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Read Uncommitted&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read Committed&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeatable Read&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Possible under SQL standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serializable&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;td&gt;Prevented&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table describes the general SQL-standard guarantees.&lt;/p&gt;

&lt;p&gt;PostgreSQL has two important differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read Uncommitted&lt;/strong&gt; behaves like Read Committed, so dirty reads are prevented.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeatable Read&lt;/strong&gt; also prevents phantom reads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lost updates need separate consideration because their behavior depends on the type of update and the database's concurrency-control mechanisms.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Understood About Isolation
&lt;/h3&gt;

&lt;p&gt;Isolation is not simply about making transactions run one after another.&lt;/p&gt;

&lt;p&gt;It is about controlling how concurrent transactions interact.&lt;/p&gt;

&lt;p&gt;Different isolation levels provide different guarantees, and the right choice depends on what our application needs.&lt;/p&gt;

&lt;p&gt;For many applications, Read Committed is sufficient.&lt;/p&gt;

&lt;p&gt;But when multiple queries need to see a consistent snapshot, Repeatable Read may be useful.&lt;/p&gt;

&lt;p&gt;And when concurrent transactions must behave like some serial execution, Serializable provides the strongest guarantee.&lt;/p&gt;

&lt;p&gt;The important thing is to understand what problems each isolation level prevents and choose accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Durability
&lt;/h2&gt;

&lt;p&gt;Durability means that once a transaction is committed, its changes should not be lost, even if the database crashes or restarts.&lt;/p&gt;

&lt;p&gt;Let's take our restaurant example.&lt;/p&gt;

&lt;p&gt;Suppose a customer finishes eating, pays the bill, and the waiter confirms the payment in the billing system.&lt;/p&gt;

&lt;p&gt;The database successfully commits the transaction.&lt;/p&gt;

&lt;p&gt;But immediately after that, the database server crashes.&lt;/p&gt;

&lt;p&gt;When the server restarts, the payment information should still be there.&lt;/p&gt;

&lt;p&gt;This is what &lt;strong&gt;Durability&lt;/strong&gt; guarantees.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do databases maintain durability?
&lt;/h3&gt;

&lt;p&gt;Databases use different techniques to make sure committed changes survive failures.&lt;/p&gt;

&lt;p&gt;Some common techniques are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write-Ahead Logging (WAL)&lt;/li&gt;
&lt;li&gt;OS Page Cache and fsync&lt;/li&gt;
&lt;li&gt;Checkpoints&lt;/li&gt;
&lt;li&gt;Asynchronous Snapshots&lt;/li&gt;
&lt;li&gt;Append-Only Files (AOF)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  1. Write-Ahead Logging (WAL)
&lt;/h3&gt;

&lt;p&gt;Writing every change directly to the database files can be expensive.&lt;/p&gt;

&lt;p&gt;For example, when we update a record, the database may need to modify multiple data pages and indexes.&lt;/p&gt;

&lt;p&gt;Instead of writing all these changes to their final locations immediately, databases like PostgreSQL use &lt;strong&gt;Write-Ahead Logging (WAL)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The idea is simple.&lt;/p&gt;

&lt;p&gt;Before modified data pages are written to disk, the database first records the changes in a log.&lt;/p&gt;

&lt;p&gt;Let's see how PostgreSQL handles an UPDATE 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;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;payment_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Paid'&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&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;When PostgreSQL executes this query, it modifies the data page in shared buffers and creates the required WAL records in WAL buffers.&lt;/p&gt;

&lt;p&gt;Both are initially in RAM.&lt;/p&gt;

&lt;p&gt;Under normal synchronous commit settings, PostgreSQL persists the required WAL before confirming the transaction.&lt;/p&gt;

&lt;p&gt;The modified data pages can be written to disk later.&lt;/p&gt;

&lt;p&gt;Here is how the process works.&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%2Fl9t8wq5k3n4c55e1e45m.jpg" 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%2Fl9t8wq5k3n4c55e1e45m.jpg" alt=" " width="800" height="788"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now suppose the database crashes after COMMIT, before the modified data pages are written to disk.&lt;/p&gt;

&lt;p&gt;When PostgreSQL restarts, it can use the persisted WAL records to recover the committed changes.&lt;/p&gt;

&lt;p&gt;This is why WAL plays an important role in durability.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. OS Page Cache and fsync
&lt;/h3&gt;

&lt;p&gt;One interesting thing I learned is that &lt;strong&gt;writing data to a file doesn't always mean it has reached the physical disk&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Usually, when a program writes to a file, the operating system may first keep that data in its page cache, which is part of RAM.&lt;/p&gt;

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

&lt;p&gt;Because writing to RAM is much faster than writing directly to disk. The operating system can write the cached data to disk later.&lt;/p&gt;

&lt;p&gt;But how is this related to database durability?&lt;/p&gt;

&lt;p&gt;Let's take our PostgreSQL example.&lt;/p&gt;

&lt;p&gt;When PostgreSQL executes an UPDATE query, it modifies the data in shared buffers and generates WAL records in WAL buffers.&lt;/p&gt;

&lt;p&gt;Both are initially in RAM.&lt;/p&gt;

&lt;p&gt;When PostgreSQL writes these WAL records to its WAL file, they may first go through the operating system's page cache.&lt;/p&gt;

&lt;p&gt;So even though PostgreSQL has written the WAL to a file, the data might still be sitting in RAM.&lt;/p&gt;

&lt;p&gt;The flow looks like this:&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%2Fxlmsqbm42y05kpgeobr9.jpg" 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%2Fxlmsqbm42y05kpgeobr9.jpg" alt=" " width="800" height="894"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now suppose the WAL has been written to the OS page cache, but it hasn't reached the disk yet.&lt;/p&gt;

&lt;p&gt;If the machine suddenly loses power, the cached WAL data may be lost.&lt;/p&gt;

&lt;p&gt;And if the modified data page was also only in RAM, PostgreSQL might lose that change.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;fsync()&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fsync()&lt;/code&gt; is an operating system call that requests the modified data of a file to be synchronized to stable storage.&lt;/p&gt;

&lt;p&gt;In simple terms, it helps make sure the data isn't just sitting in the OS page cache.&lt;/p&gt;

&lt;p&gt;Under normal durability settings, PostgreSQL uses &lt;code&gt;fsync()&lt;/code&gt; or other appropriate synchronization methods to persist the required WAL before confirming a synchronous commit.&lt;/p&gt;

&lt;p&gt;So even if the database crashes immediately after COMMIT, PostgreSQL can use the persisted WAL to recover the changes.&lt;/p&gt;

&lt;p&gt;The important difference is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WAL records the changes needed for recovery, while fsync or a similar synchronization mechanism helps make those WAL records durable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There is also a performance cost. Synchronizing data to storage takes time, so frequent synchronization can increase write latency.&lt;/p&gt;

&lt;p&gt;PostgreSQL provides settings such as &lt;code&gt;fsync&lt;/code&gt; and &lt;code&gt;synchronous_commit&lt;/code&gt; to control this behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Checkpoints
&lt;/h3&gt;

&lt;p&gt;We have seen that PostgreSQL can commit a transaction before writing all the modified data pages to their final locations.&lt;/p&gt;

&lt;p&gt;But those modified pages cannot remain only in memory forever.&lt;/p&gt;

&lt;p&gt;They eventually need to be written to disk.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;Checkpoints&lt;/strong&gt; come in.&lt;/p&gt;

&lt;p&gt;A checkpoint is a process where PostgreSQL ensures that dirty data pages (modified in memory but not yet on disk) are written to storage and records a point from which crash recovery can proceed.&lt;/p&gt;

&lt;p&gt;For example, suppose the database has been running for several hours and thousands of transactions have modified data.&lt;/p&gt;

&lt;p&gt;If the database crashes, PostgreSQL needs to replay WAL records to recover changes that weren't already reflected in the database files.&lt;/p&gt;

&lt;p&gt;Without checkpoints, the amount of WAL that needs to be processed during recovery could become very large.&lt;/p&gt;

&lt;p&gt;By performing checkpoints periodically, PostgreSQL can reduce the amount of recovery work required.&lt;/p&gt;

&lt;p&gt;So checkpoints mainly help with writing modified data pages to disk and controlling crash recovery time.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Asynchronous Snapshots
&lt;/h3&gt;

&lt;p&gt;Some databases use snapshots as a persistence mechanism.&lt;/p&gt;

&lt;p&gt;Instead of saving every change immediately, they periodically save a snapshot of their data.&lt;/p&gt;

&lt;p&gt;For example, Redis supports RDB snapshots.&lt;/p&gt;

&lt;p&gt;Suppose a database takes a snapshot at 10:00 AM.&lt;/p&gt;

&lt;p&gt;After that, some new records are created at 10:05 AM.&lt;/p&gt;

&lt;p&gt;Now imagine the server crashes at 10:06 AM, before the next snapshot is taken.&lt;/p&gt;

&lt;p&gt;If snapshots are the only persistence mechanism, those recent changes may be lost.&lt;/p&gt;

&lt;p&gt;So the durability guarantee depends on how frequently snapshots are taken and whether other persistence methods are enabled.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Append-Only Files (AOF)
&lt;/h3&gt;

&lt;p&gt;Another persistence technique is an &lt;strong&gt;Append-Only File (AOF)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Redis supports this mechanism.&lt;/p&gt;

&lt;p&gt;Instead of saving the entire dataset after every update, Redis records write operations in an append-only log.&lt;/p&gt;

&lt;p&gt;When the server restarts, Redis can use the persisted AOF to rebuild its data.&lt;/p&gt;

&lt;p&gt;However, durability depends on how frequently the AOF is synchronized to disk.&lt;/p&gt;

&lt;p&gt;For example, Redis can be configured to synchronize after every write or approximately once per second.&lt;/p&gt;

&lt;p&gt;So AOF provides another way to recover data, but recent changes may still be lost depending on the persistence settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;When I started learning about database transactions, I thought it was mostly about BEGIN, COMMIT, and ROLLBACK.&lt;/p&gt;

&lt;p&gt;But as I went deeper, I came across some interesting things, especially what happens when two transactions run at the same time and how PostgreSQL handles failures.&lt;/p&gt;

&lt;p&gt;There is still a lot more to learn about how databases work internally, but understanding these concepts helped me see what actually happens behind a simple database query.&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>OpenSearch Under the Hood: How It Actually Works</title>
      <dc:creator>Anusha Renangi</dc:creator>
      <pubDate>Sun, 04 Oct 2026 15:31:30 +0000</pubDate>
      <link>https://dev.to/anusha_renangi_05a4e41d35/opensearch-under-the-hood-how-it-actually-works-4j1i</link>
      <guid>https://dev.to/anusha_renangi_05a4e41d35/opensearch-under-the-hood-how-it-actually-works-4j1i</guid>
      <description>&lt;p&gt;I have been using OpenSearch for querying and aggregating data at work. Initially, I was just maintaining the code, but after working with it for some time, I started wondering what actually happens behind those queries.&lt;/p&gt;

&lt;p&gt;When I search for something, does OpenSearch search the original JSON documents?&lt;/p&gt;

&lt;p&gt;And when an aggregation runs across multiple shards, how does OpenSearch get one final result?&lt;/p&gt;

&lt;p&gt;So I decided to understand OpenSearch from the inside rather than treating it as just another API that returns search results.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;First, why do we need OpenSearch?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Let’s say an application has millions of documents.&lt;/p&gt;

&lt;p&gt;We may want to do things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search text&lt;/li&gt;
&lt;li&gt;Filter documents&lt;/li&gt;
&lt;li&gt;Sort by fields&lt;/li&gt;
&lt;li&gt;Group documents&lt;/li&gt;
&lt;li&gt;Calculate aggregations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Doing this efficiently over a large amount of data is not the same as simply storing JSON and scanning every document whenever a query comes in.&lt;/p&gt;

&lt;p&gt;OpenSearch is designed for search and analytics over large datasets.&lt;/p&gt;

&lt;p&gt;To understand how it does this, let’s start with how the data is organized.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Cluster → Nodes → Indexes → Shards → Documents&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;These terms are easy to mix up when we first start working with OpenSearch.&lt;/p&gt;

&lt;p&gt;Here is the simplest way I understand them now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cluster
&lt;/h2&gt;

&lt;p&gt;A cluster is a collection of OpenSearch nodes that work together.&lt;/p&gt;

&lt;p&gt;A cluster can contain multiple indexes, and each index can be divided into multiple shards that are distributed across the cluster’s nodes.&lt;/p&gt;

&lt;p&gt;You can loosely compare an OpenSearch cluster to a database system in the relational-database world, but they are not exactly the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Node
&lt;/h2&gt;

&lt;p&gt;A node is an OpenSearch server that is part of the cluster.&lt;/p&gt;

&lt;p&gt;Nodes provide the compute and storage where shards are placed.&lt;/p&gt;

&lt;p&gt;A cluster can have multiple nodes, and a node can hold multiple shards from one or more indexes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Index
&lt;/h2&gt;

&lt;p&gt;An index is a logical collection of related documents.&lt;/p&gt;

&lt;p&gt;It is somewhat similar to a table in a relational database.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For example:&lt;br&gt;
orders&lt;br&gt;
products&lt;br&gt;
users&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An index can contain a large number of documents, so OpenSearch divides the index into shards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shard
&lt;/h2&gt;

&lt;p&gt;A shard is a partition of an index.&lt;/p&gt;

&lt;p&gt;These shards can be distributed across different nodes in the cluster.&lt;/p&gt;

&lt;p&gt;Each shard contains a subset of the documents belonging to the index.&lt;/p&gt;

&lt;h2&gt;
  
  
  Document
&lt;/h2&gt;

&lt;p&gt;A document is the actual piece of data stored in the index.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For example:&lt;br&gt;
{&lt;br&gt;
“name”: “Laptop”,&lt;br&gt;
“region”: “India”,&lt;br&gt;
“amount”: 75000&lt;br&gt;
}&lt;/p&gt;
&lt;/blockquote&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%2F0i6s3oqi6jrlzmkae1hg.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%2F0i6s3oqi6jrlzmkae1hg.png" alt="Opensearch High level Storage Structure" width="800" height="549"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually lives inside a shard?
&lt;/h2&gt;

&lt;p&gt;A shard is backed by Apache Lucene.&lt;/p&gt;

&lt;p&gt;And OpenSearch doesn’t simply keep our JSON documents somewhere and scan them whenever we search.&lt;/p&gt;

&lt;p&gt;It maintains different structures that are useful for different operations.&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%2Fj3iym2dag135x52kcap2.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%2Fj3iym2dag135x52kcap2.png" alt="Shard Internal Structure" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Inverted Index
&lt;/h2&gt;

&lt;p&gt;Let’s start with search.&lt;/p&gt;

&lt;p&gt;Suppose we have these documents:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Document 1: “I love OpenSearch”&lt;br&gt;
Document 2: “OpenSearch is fast”&lt;br&gt;
Document 3: “I love search”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If we stored only the documents in their original form, one simple way to search would be to look through the documents and check which ones contain the word we are searching for.&lt;/p&gt;

&lt;p&gt;An inverted index changes the way we look at the data.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Instead of:&lt;br&gt;
Document → Words&lt;br&gt;
we have something more like:&lt;br&gt;
Word → Documents containing that word&lt;br&gt;
For example:&lt;br&gt;
I → 1, 3&lt;br&gt;
love → 1, 3&lt;br&gt;
OpenSearch → 1, 2&lt;br&gt;
is → 2&lt;br&gt;
fast → 2&lt;br&gt;
search → 3&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now if the query is: “OpenSearch”&lt;/p&gt;

&lt;p&gt;the inverted index can tell us: OpenSearch → Document 1, Document 2&lt;/p&gt;

&lt;p&gt;We don’t have to think of search as scanning every JSON document anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doc Values
&lt;/h2&gt;

&lt;p&gt;Search is only one type of operation.&lt;/p&gt;

&lt;p&gt;Suppose I want to run: SUM(amount)&lt;/p&gt;

&lt;p&gt;or sort documents by: amount&lt;/p&gt;

&lt;p&gt;For these operations, we need efficient access to field values.&lt;/p&gt;

&lt;p&gt;A simplified way to think about them is:&lt;/p&gt;

&lt;p&gt;Document amount&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Doc 1 75000&lt;br&gt;
Doc 2 45000&lt;br&gt;
Doc 3 90000&lt;br&gt;
Doc 4 30000&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  _source
&lt;/h2&gt;

&lt;p&gt;Then there is the original JSON.&lt;/p&gt;

&lt;p&gt;Suppose we indexed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;{&lt;br&gt;
“name”: “Laptop”,&lt;br&gt;
“region”: “India”,&lt;br&gt;
“amount”: 75000&lt;br&gt;
}&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When we retrieve the document, we generally want to get something close to what we originally sent.&lt;/p&gt;

&lt;p&gt;That’s what _source is for.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“_source”: {&lt;br&gt;
“name”: “Laptop”,&lt;br&gt;
“region”: “India”,&lt;br&gt;
“amount”: 75000&lt;br&gt;
}&lt;br&gt;
It isn’t simply storing JSON documents and searching through them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Different data structures are maintained because different operations need different ways of accessing the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when we actually send a query?
&lt;/h2&gt;

&lt;p&gt;Now let’s say our orders index has three shards.&lt;/p&gt;

&lt;p&gt;Suppose we send a search query.&lt;/p&gt;

&lt;p&gt;The request reaches a node, which coordinates the request.&lt;/p&gt;

&lt;p&gt;The query is sent to the relevant shards.&lt;/p&gt;

&lt;p&gt;Each shard searches its own local data.&lt;/p&gt;

&lt;p&gt;Then the results come back.&lt;/p&gt;

&lt;p&gt;The coordinating node merges the shard results and produces the final response.&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%2Fzc494jz0s2vagnbnvzyq.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%2Fzc494jz0s2vagnbnvzyq.png" alt="Opensearch Query Process" width="799" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  But does OpenSearch always give us the exact global result?
&lt;/h2&gt;

&lt;p&gt;Suppose we run an aggregation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Group documents by region&lt;br&gt;
Return the top 2 regions by document count&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The documents are distributed across multiple shards.&lt;/p&gt;

&lt;p&gt;Each shard does its own local processing and sends results back to the coordinating node.&lt;/p&gt;

&lt;p&gt;The coordinating node then combines those results.&lt;/p&gt;

&lt;p&gt;A value that isn’t among the top candidates from one shard can still become important when the results from all shards are combined.&lt;/p&gt;

&lt;p&gt;For example, a region might have a lower count on each individual shard, but when those counts are combined across all shards, that region could have a high global count.&lt;/p&gt;

&lt;p&gt;So how can OpenSearch confidently say, &lt;em&gt;“These are the top results across the entire cluster”?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That’s where distributed aggregation gets tricky and sometimes, approximate.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In the next article, we’ll see exactly why this happens and how OpenSearch improves the accuracy of these results.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensearch</category>
      <category>systemdesign</category>
      <category>database</category>
    </item>
  </channel>
</rss>
