<?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: Rahul Dhawan</title>
    <description>The latest articles on DEV Community by Rahul Dhawan (@r4hul).</description>
    <link>https://dev.to/r4hul</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%2F4044180%2F75e0ea0c-63e0-4296-8acd-c1343fbd4153.png</url>
      <title>DEV Community: Rahul Dhawan</title>
      <link>https://dev.to/r4hul</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/r4hul"/>
    <language>en</language>
    <item>
      <title>Google Cloud Pub/Sub Explained: Topics, Subscriptions, ACKs, and NACKs</title>
      <dc:creator>Rahul Dhawan</dc:creator>
      <pubDate>Fri, 14 Aug 2026 18:42:54 +0000</pubDate>
      <link>https://dev.to/r4hul/google-cloud-pubsub-explained-topics-subscriptions-acks-and-nacks-3age</link>
      <guid>https://dev.to/r4hul/google-cloud-pubsub-explained-topics-subscriptions-acks-and-nacks-3age</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Your checkout service should not have to wait for analytics, email, inventory, and fraud-detection systems before responding to a customer. Google Cloud Pub/Sub helps these systems communicate asynchronously without tightly coupling them together.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Modern applications generate a constant stream of events: orders are placed, files are uploaded, devices report measurements, and users click buttons. Processing every event synchronously can make an application slow and fragile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Cloud Pub/Sub&lt;/strong&gt; is a managed messaging service for exchanging these events reliably and at scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Pub/Sub?
&lt;/h2&gt;

&lt;p&gt;Pub/Sub follows the &lt;strong&gt;publish-subscribe&lt;/strong&gt; pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;publisher&lt;/strong&gt; produces a message.&lt;/li&gt;
&lt;li&gt;The publisher sends it to a &lt;strong&gt;topic&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;subscription&lt;/strong&gt; represents a consumer's interest in that topic.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;subscriber&lt;/strong&gt; receives and processes messages through the subscription.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The publisher does not need to know which services consume the event or whether they are currently online. This separation is called &lt;strong&gt;decoupling&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%2Frse3we6ii8lffq79qhgi.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%2Frse3we6ii8lffq79qhgi.png" alt=" " width="799" height="213"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each subscription receives its own copy of a topic's messages. If multiple subscriber instances consume from the &lt;strong&gt;same subscription&lt;/strong&gt;, they share the work instead of each receiving a copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;p&gt;Consider an online store:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The order service publishes an &lt;code&gt;order.created&lt;/code&gt; event.&lt;/li&gt;
&lt;li&gt;Pub/Sub stores and routes the message to subscriptions attached to the topic.&lt;/li&gt;
&lt;li&gt;Email, inventory, and analytics subscribers process the event independently.&lt;/li&gt;
&lt;li&gt;Each subscriber acknowledges successful processing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the analytics system is temporarily unavailable, checkout can still succeed. Pub/Sub retains the unacknowledged message and attempts delivery again.&lt;/p&gt;

&lt;p&gt;A typical message contains a payload plus optional attributes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1042"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C-51"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;89.99&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attributes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eventType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order.created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"checkout-service"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attributes are useful for metadata and subscription filtering, while the data field carries the main event payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull versus push subscriptions
&lt;/h2&gt;

&lt;p&gt;Pub/Sub supports different delivery approaches, but &lt;strong&gt;pull&lt;/strong&gt; and &lt;strong&gt;push&lt;/strong&gt; are the most common.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;How delivery starts&lt;/th&gt;
&lt;th&gt;Good fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pull&lt;/td&gt;
&lt;td&gt;The subscriber requests messages&lt;/td&gt;
&lt;td&gt;Workers that need control over concurrency, batching, and processing rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push&lt;/td&gt;
&lt;td&gt;Pub/Sub sends an HTTPS request to an endpoint&lt;/td&gt;
&lt;td&gt;Webhooks, Cloud Run services, and simple serverless consumers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Google's high-level client libraries commonly use &lt;strong&gt;StreamingPull&lt;/strong&gt;, maintaining an open connection and delivering messages asynchronously.&lt;/p&gt;

&lt;p&gt;With push delivery, a successful HTTP response acknowledges the message. A failure response or no successful response before the deadline causes Pub/Sub to retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  ACK:  Processing succeeded
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;ACK&lt;/strong&gt;, or acknowledgment, tells Pub/Sub:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I successfully processed this message. It no longer needs to be delivered on this subscription.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a Python pull subscriber, the essential pattern looks like this:&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;callback&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="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;process_order&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="n"&gt;data&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="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&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="nf"&gt;nack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important rule is: &lt;strong&gt;ACK only after the required work succeeds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Acknowledging before writing to a database or calling a required downstream service can cause data loss from the application's perspective. If the process crashes after the early ACK, Pub/Sub considers the message complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  NACK:  Try this again
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;NACK&lt;/strong&gt;, or negative acknowledgment, tells Pub/Sub that processing did not complete and the message should become eligible for redelivery.&lt;/p&gt;

&lt;p&gt;Useful reasons to NACK include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A database is temporarily unavailable.&lt;/li&gt;
&lt;li&gt;A dependency returns a retryable error.&lt;/li&gt;
&lt;li&gt;The subscriber is shutting down before processing finishes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A NACK does &lt;strong&gt;not&lt;/strong&gt; mean  discard this message. It normally means  redeliver it. In the lower-level API, a NACK is represented by setting the message's acknowledgment deadline to zero.&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%2Fqabt7dyd28b4syigbnl3.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%2Fqabt7dyd28b4syigbnl3.png" alt=" " width="800" height="638"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the acknowledgment deadline?
&lt;/h2&gt;

&lt;p&gt;After delivering a message, Pub/Sub gives the subscriber a limited period called the &lt;strong&gt;acknowledgment deadline&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;During that time, the message is considered outstanding. If it is not acknowledged before the deadline expires, Pub/Sub can deliver it again possibly to another subscriber instance using the same subscription.&lt;/p&gt;

&lt;p&gt;High-level client libraries can automatically extend the deadline while a callback is still processing. Even so, subscribers should avoid unbounded work and should monitor expired acknowledgment deadlines.&lt;/p&gt;

&lt;p&gt;Because redelivery can occur, handlers should be &lt;strong&gt;idempotent&lt;/strong&gt;: processing the same message more than once should not create an incorrect result.&lt;/p&gt;

&lt;p&gt;For example, use &lt;code&gt;orderId&lt;/code&gt; as an idempotency key before charging a card or creating a shipment.&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%2F0ojhf1kp06uu4cw1fcw4.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%2F0ojhf1kp06uu4cw1fcw4.png" alt=" " width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pub/Sub also offers an &lt;strong&gt;exactly-once delivery&lt;/strong&gt; option for pull subscriptions, but application-level idempotency remains a valuable defense against failures outside the messaging service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retries and poison messages
&lt;/h2&gt;

&lt;p&gt;Not every failure is temporary. A malformed event may fail every time it is delivered. Repeatedly NACKing it can waste resources and delay useful work.&lt;/p&gt;

&lt;p&gt;For production systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configure an exponential-backoff retry policy for temporary failures.&lt;/li&gt;
&lt;li&gt;Configure a dead-letter topic for messages that exceed the allowed delivery attempts.&lt;/li&gt;
&lt;li&gt;Alert on growing subscription backlogs and expired ACK deadlines.&lt;/li&gt;
&lt;li&gt;ACK intentionally ignored messages instead of repeatedly NACKing them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A dead-letter subscriber can inspect failed events without blocking the main processing path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where is Pub/Sub useful?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Event-driven microservices
&lt;/h3&gt;

&lt;p&gt;An order event can trigger inventory, notifications, loyalty points, and analytics without adding direct dependencies to the checkout service.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Streaming data ingestion
&lt;/h3&gt;

&lt;p&gt;Applications and IoT devices can publish events that are processed by Dataflow or loaded into analytics systems such as BigQuery.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Background work and burst absorption
&lt;/h3&gt;

&lt;p&gt;A service can publish work faster than workers can briefly process it. The subscription backlog acts as a buffer while workers scale out or catch up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why choose it over alternatives?
&lt;/h2&gt;

&lt;p&gt;Choose Pub/Sub when you want a &lt;strong&gt;managed, scalable event bus&lt;/strong&gt;, asynchronous communication, and easy one-to-many fan-out without managing messaging servers.&lt;/p&gt;

&lt;p&gt;However, it is not automatically the best choice for every asynchronous task:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose &lt;strong&gt;Cloud Tasks&lt;/strong&gt; when the producer must target a specific endpoint and needs task-level scheduling or delivery-rate control.&lt;/li&gt;
&lt;li&gt;Consider &lt;strong&gt;Apache Kafka&lt;/strong&gt; when your architecture requires direct control over partitions, brokers, or log-oriented consumption patterns and your team accepts the added operational model.&lt;/li&gt;
&lt;li&gt;Use a synchronous API when the caller needs an immediate result before it can continue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key question is not  Which tool is most powerful? It is  Which delivery and ownership model matches the problem?&lt;/p&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;Pub/Sub creates a reliable boundary between event producers and consumers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Topics&lt;/strong&gt; receive messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subscriptions&lt;/strong&gt; create independent delivery streams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ACK&lt;/strong&gt; confirms successful processing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NACK&lt;/strong&gt; requests redelivery after a failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ack deadlines, retries, idempotency, and dead-letter topics&lt;/strong&gt; make failure handling explicit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once ACK and NACK behavior is clear, Pub/Sub becomes much easier to reason about and event-driven systems become easier to scale without turning every service into a dependency of every other service.&lt;/p&gt;

</description>
      <category>gcp</category>
      <category>pubsub</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Caching Without the Confusion: Patterns, Trade-offs, and When to Use Each</title>
      <dc:creator>Rahul Dhawan</dc:creator>
      <pubDate>Sat, 01 Aug 2026 10:07:29 +0000</pubDate>
      <link>https://dev.to/r4hul/caching-without-the-confusion-patterns-trade-offs-and-when-to-use-each-47p1</link>
      <guid>https://dev.to/r4hul/caching-without-the-confusion-patterns-trade-offs-and-when-to-use-each-47p1</guid>
      <description>&lt;h2&gt;
  
  
  Caching Without the Confusion: Patterns, Trade-offs, and When to Use Each
&lt;/h2&gt;

&lt;p&gt;Caching is one of the simplest ways to make an application feel dramatically faster until stale data, cache misses, or invalidation problems appear.&lt;/p&gt;

&lt;p&gt;At its core, a cache stores frequently accessed data in a faster storage layer so the application does not need to repeatedly query a slower source such as a database, external API, or filesystem.&lt;/p&gt;

&lt;p&gt;This article explains the most common caching types and strategies, along with practical guidance on when to use each one.&lt;/p&gt;

&lt;h2&gt;
  
  
  How caching works
&lt;/h2&gt;

&lt;p&gt;A typical cached read follows this flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The application requests data.&lt;/li&gt;
&lt;li&gt;It checks the cache using a unique key.&lt;/li&gt;
&lt;li&gt;On a &lt;strong&gt;cache hit&lt;/strong&gt;, the cached value is returned.&lt;/li&gt;
&lt;li&gt;On a &lt;strong&gt;cache miss&lt;/strong&gt;, the application retrieves the value from the source of truth.&lt;/li&gt;
&lt;li&gt;The value may then be cached for future requests.&lt;/li&gt;
&lt;/ol&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%2Fll5xpl5ozlddwp40ckrb.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%2Fll5xpl5ozlddwp40ckrb.png" alt="How cache works" width="800" height="108"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A useful cache depends on three decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache key:&lt;/strong&gt; How is the value identified? Example: &lt;code&gt;product:123&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expiration:&lt;/strong&gt; How long can the value remain cached?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eviction:&lt;/strong&gt; What happens when the cache runs out of space?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Common eviction policies include &lt;strong&gt;LRU&lt;/strong&gt; (least recently used), &lt;strong&gt;LFU&lt;/strong&gt; (least frequently used), and TTL-based expiration.&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%2Fmme13am5gbjgqp8aklwf.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%2Fmme13am5gbjgqp8aklwf.jpg" alt="Cache meme" width="800" height="707"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  In-memory vs. distributed caching
&lt;/h2&gt;

&lt;h3&gt;
  
  
  In-memory cache
&lt;/h3&gt;

&lt;p&gt;An in-memory cache lives inside a single application process. Examples include a language-native map, an LRU cache library, or a framework's local cache.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Extremely fast&lt;/li&gt;
&lt;li&gt;Simple to implement&lt;/li&gt;
&lt;li&gt;No network call&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Each application instance has its own copy&lt;/li&gt;
&lt;li&gt;Cache contents disappear when the process restarts&lt;/li&gt;
&lt;li&gt;Memory is limited to the host&lt;/li&gt;
&lt;li&gt;Data may be inconsistent across instances&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use it when:&lt;/strong&gt; the data is local, inexpensive to rebuild, and does not need to be shared for example, parsed configuration, static reference data, or short-lived request metadata.&lt;/p&gt;

&lt;h1&gt;
  
  
  Caching starts at the hardware level.
&lt;/h1&gt;

&lt;p&gt;This diagram shows the typical CPU cache hierarchy: each core has small, fast private L1 and L2 caches, while a larger L3 cache is shared across cores.&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%2Flrpya3oyptvhq3vllsmg.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%2Flrpya3oyptvhq3vllsmg.png" alt="CPU Cache hierarchy" width="800" height="1193"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Distributed cache
&lt;/h3&gt;

&lt;p&gt;A distributed cache runs outside the application and can be shared by multiple instances. Redis and Memcached are common examples.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Shared across application instances&lt;/li&gt;
&lt;li&gt;Can scale independently&lt;/li&gt;
&lt;li&gt;Better consistency across a distributed system&lt;/li&gt;
&lt;li&gt;May support replication and persistence&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Adds network latency&lt;/li&gt;
&lt;li&gt;Requires infrastructure and monitoring&lt;/li&gt;
&lt;li&gt;Can become a dependency or bottleneck&lt;/li&gt;
&lt;li&gt;Serialization adds overhead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use it when:&lt;/strong&gt; several services or application instances need access to the same cached data, such as user sessions, product details, rate-limit counters, or API responses.&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%2Ftxzz3keoh08k2fktll8a.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%2Ftxzz3keoh08k2fktll8a.png" alt="Distributed vs In-memory caching" width="800" height="209"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Common caching strategies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Cache-aside (lazy loading)
&lt;/h3&gt;

&lt;p&gt;With cache-aside, the application manages the cache directly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read from the cache first.&lt;/li&gt;
&lt;li&gt;If the value is missing, read from the database.&lt;/li&gt;
&lt;li&gt;Store the result in the cache.&lt;/li&gt;
&lt;li&gt;Return the value.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuznyiy6hae8w70w7mjq4.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%2Fuznyiy6hae8w70w7mjq4.png" alt="Cache sequence diagram" width="800" height="670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; read-heavy workloads where not every record needs to be cached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; An e-commerce site caches popular product pages. Products are added to the cache only after someone requests them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch out for:&lt;/strong&gt; stale data and repeated misses during traffic spikes. Invalidate or update the cache after database writes, and consider request coalescing to prevent a cache stampede.&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%2Fx4d3i2mdas348o3vnhbd.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%2Fx4d3i2mdas348o3vnhbd.jpg" alt="Cache meme 2" width="800" height="902"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Read-through
&lt;/h3&gt;

&lt;p&gt;Read-through looks similar to cache-aside, but the cache providerâ€”not the applicationâ€”loads missing values from the database.&lt;/p&gt;

&lt;p&gt;The application always asks the cache for data. On a miss, the cache invokes a configured loader, stores the result, and returns it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; applications that want simpler read logic and have a caching library or platform that supports data loaders.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; A service repeatedly loads customer profiles. A read-through abstraction keeps database-loading logic out of business code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; the cache layer becomes more tightly coupled to the data source and its loading rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Write-through
&lt;/h3&gt;

&lt;p&gt;With write-through, every write is synchronously sent to both the cache and the database. The operation succeeds only after the backing store is updated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; data that is read frequently after being updated and where cache freshness matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; A user updates their account preferences. Writing to both layers ensures the next request sees the latest settings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; writes have higher latency, and rarely read data may consume cache space.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Write-behind (write-back)
&lt;/h3&gt;

&lt;p&gt;With write-behind, the application writes to the cache first. The cache acknowledges the request and updates the database asynchronously, often in batches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; write-heavy workloads where throughput is more important than immediate durability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; A gaming platform records rapidly changing counters or activity metrics and periodically flushes them to durable storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; data can be lost if the cache fails before pending writes reach the database. This strategy requires durable queues, retries, monitoring, and clear failure handling.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Write-around
&lt;/h3&gt;

&lt;p&gt;With write-around, the application writes directly to the database without updating the cache. Future reads load the new value into the cache if needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; write-heavy data that may never be read soon after it is written.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; An audit system stores large event records, while only a small percentage are later viewed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; the first read after a write is a cache miss. Existing cached values must also be invalidated to avoid stale reads.&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%2Fad4ys4i3dqtkmhd70l8h.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%2Fad4ys4i3dqtkmhd70l8h.png" alt="Trade-off flow chart" width="800" height="1066"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Who loads or writes the cache?&lt;/th&gt;
&lt;th&gt;Main strength&lt;/th&gt;
&lt;th&gt;Main risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cache-aside&lt;/td&gt;
&lt;td&gt;Application&lt;/td&gt;
&lt;td&gt;Flexible and widely supported&lt;/td&gt;
&lt;td&gt;Stale data and miss storms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read-through&lt;/td&gt;
&lt;td&gt;Cache layer&lt;/td&gt;
&lt;td&gt;Simpler application reads&lt;/td&gt;
&lt;td&gt;Provider coupling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write-through&lt;/td&gt;
&lt;td&gt;Cache and database synchronously&lt;/td&gt;
&lt;td&gt;Fresh cache after writes&lt;/td&gt;
&lt;td&gt;Higher write latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write-behind&lt;/td&gt;
&lt;td&gt;Cache first; database later&lt;/td&gt;
&lt;td&gt;High write throughput&lt;/td&gt;
&lt;td&gt;Possible data loss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write-around&lt;/td&gt;
&lt;td&gt;Database; cache populated later&lt;/td&gt;
&lt;td&gt;Avoids caching cold writes&lt;/td&gt;
&lt;td&gt;Read-after-write miss&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Production pitfalls to plan for
&lt;/h2&gt;

&lt;p&gt;Caching moves complexity rather than removing it. Before shipping, consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TTL jitter:&lt;/strong&gt; Add small randomness to expiration times so many keys do not expire simultaneously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache stampede protection:&lt;/strong&gt; Allow one request to refresh a missing value while others wait or receive slightly stale data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Negative caching:&lt;/strong&gt; Briefly cache â€œnot foundâ€ results to protect the database from repeated invalid requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invalidation:&lt;/strong&gt; Delete or update affected keys whenever the source data changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability:&lt;/strong&gt; Track hit rate, miss rate, latency, evictions, memory usage, and errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure behavior:&lt;/strong&gt; Decide whether the application should bypass the cache or fail when it is unavailable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; Do not place secrets or sensitive data in a cache without appropriate encryption and access controls.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;There is no universally best caching strategy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start with &lt;strong&gt;cache-aside&lt;/strong&gt; for most read-heavy applications.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;read-through&lt;/strong&gt; when your caching platform can cleanly own data loading.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;write-through&lt;/strong&gt; when fresh cached data matters more than write speed.&lt;/li&gt;
&lt;li&gt;Consider &lt;strong&gt;write-behind&lt;/strong&gt; only when you can tolerate delayed persistence and engineer for failure.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;write-around&lt;/strong&gt; to avoid filling the cache with data unlikely to be read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good cache should reduce load without becoming a second, less reliable database. Keep the source of truth clear, define expiration and invalidation rules, and measure whether the cache is actually improving the system.&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%2Fwikpraelx7zct30nfi4t.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%2Fwikpraelx7zct30nfi4t.png" alt="Thanks for reading" width="614" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>caching</category>
      <category>architecture</category>
      <category>backend</category>
      <category>performance</category>
    </item>
    <item>
      <title>What Really Happens When PostgreSQL Runs Your Query?</title>
      <dc:creator>Rahul Dhawan</dc:creator>
      <pubDate>Thu, 23 Jul 2026 19:12:35 +0000</pubDate>
      <link>https://dev.to/r4hul/what-really-happens-when-postgresql-runs-your-query-3829</link>
      <guid>https://dev.to/r4hul/what-really-happens-when-postgresql-runs-your-query-3829</guid>
      <description>&lt;p&gt;I recently had one of those exciting engineering moments where something I had used for months finally &lt;strong&gt;clicked&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;While investigating query performance on a project, I started looking beyond the SQL and into what PostgreSQL was doing behind the scenes. Using execution plans helped me understand why schema design, indexes, and even the columns in a &lt;code&gt;SELECT&lt;/code&gt; statement can significantly affect performance.&lt;/p&gt;

&lt;p&gt;This article shares the practical mental model I learned—without going too deeply into PostgreSQL internals.&lt;/p&gt;




&lt;h2&gt;
  
  
  What happens when PostgreSQL receives a query?
&lt;/h2&gt;

&lt;p&gt;When PostgreSQL receives SQL, it does not immediately start reading rows.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parses&lt;/strong&gt; the SQL and validates its syntax.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrites&lt;/strong&gt; the query when views or rules are involved.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plans&lt;/strong&gt; possible execution strategies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Estimates the cost&lt;/strong&gt; of each strategy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Executes&lt;/strong&gt; the plan it believes will be cheapest.&lt;/li&gt;
&lt;li&gt;Returns the result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A simplified view 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%2Fe4b4wcty51rym515k35v.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%2Fe4b4wcty51rym515k35v.png" alt="Flowchart showing a SQL query moving from an application through PostgreSQL’s parser, rewriter, planner, and executor before accessing tables and indexes" width="800" height="580"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PostgreSQL uses table statistics, estimated row counts, available indexes, and expected I/O costs to choose a plan.&lt;/p&gt;

&lt;p&gt;An index existing does not guarantee that PostgreSQL will use it. The planner may decide that another approach is cheaper.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tables are stored as pages
&lt;/h2&gt;

&lt;p&gt;At the SQL level, we think of a table as a collection of rows. Internally, PostgreSQL stores rows—also called tuples—inside fixed-size blocks called &lt;strong&gt;pages&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A page is typically 8 KB.&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%2Fz80fofe5pc4woxb1qy60.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%2Fz80fofe5pc4woxb1qy60.png" alt="PostgreSQL stores tuples inside fixed-size pages rather than independently reading logical SQL rows" width="799" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When PostgreSQL retrieves data, it works with these pages. A page may already be available in memory, or it may need to be read from storage.&lt;/p&gt;

&lt;p&gt;This is one reason row width matters.&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%2Ffl0wfroehre3sl9pd4at.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%2Ffl0wfroehre3sl9pd4at.png" alt="Wider rows can reduce the number of tuples that fit on each page, potentially increasing the number of pages PostgreSQL must process" width="800" height="155"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Narrow rows allow more records to fit on a page. Wider rows may require PostgreSQL to process more pages for the same number of records.&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%2Fzoawiujp7x0w04z8epl4.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%2Fzoawiujp7x0w04z8epl4.png" alt="Agamemnon aura farming meme" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  So how does PostgreSQL find a record?
&lt;/h2&gt;

&lt;p&gt;Consider the following 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;users&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="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&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;Now 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;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;email&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL has several ways to find matching records. Three important ones are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sequential scan&lt;/li&gt;
&lt;li&gt;Index scan&lt;/li&gt;
&lt;li&gt;Bitmap scan&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Sequential scan
&lt;/h2&gt;

&lt;p&gt;A sequential scan, shown as &lt;code&gt;Seq Scan&lt;/code&gt; in an execution plan, reads the table page by page and checks visible rows against the filter.&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%2Fkv9nh8eda2271nlhehlc.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%2Fkv9nh8eda2271nlhehlc.png" alt="During a sequential scan, PostgreSQL visits table pages and evaluates each visible row against the filter" width="800" height="1100"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Conceptually, PostgreSQL:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Loads a table page.&lt;/li&gt;
&lt;li&gt;Inspects its tuples.&lt;/li&gt;
&lt;li&gt;Applies transaction visibility rules.&lt;/li&gt;
&lt;li&gt;Evaluates the &lt;code&gt;WHERE&lt;/code&gt; condition.&lt;/li&gt;
&lt;li&gt;Returns matching rows.&lt;/li&gt;
&lt;li&gt;Continues through the table.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A simplified plan may look like this:&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 users
  Filter: (status = 'active')
  Rows Removed by Filter: 800000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A sequential scan is not automatically bad. It can be efficient when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The table is small.&lt;/li&gt;
&lt;li&gt;The query returns a large percentage of the table.&lt;/li&gt;
&lt;li&gt;The filter is not selective.&lt;/li&gt;
&lt;li&gt;No useful index exists.&lt;/li&gt;
&lt;li&gt;Reading the table sequentially is cheaper than performing many separate lookups.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If almost every user is active, scanning the table may be cheaper than using an index to fetch nearly every row.&lt;/p&gt;




&lt;h2&gt;
  
  
  Index scan
&lt;/h2&gt;

&lt;p&gt;An index is a separate data structure that helps PostgreSQL locate records without scanning the entire table.&lt;/p&gt;

&lt;p&gt;PostgreSQL uses B-tree indexes by default:&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_users_email&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simplified B-tree index 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%2Fej8rkl0m8cxtzbm349wg.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%2Fej8rkl0m8cxtzbm349wg.png" alt="A simplified B-tree. PostgreSQL follows the ordered tree to locate matching keys without scanning every table row" width="799" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For 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;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;email&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'alice@example.com'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ol&gt;
&lt;li&gt;Search the index for the email.&lt;/li&gt;
&lt;li&gt;Find the corresponding tuple location.&lt;/li&gt;
&lt;li&gt;Fetch the relevant table page.&lt;/li&gt;
&lt;li&gt;Check whether the row is visible.&lt;/li&gt;
&lt;li&gt;Return the requested columns.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                      B-tree index
                           |
             search for alice@example.com
                           |
                           v
                  tuple location: (42, 7)
                           |
                           v
                    Table page 42
                           |
                           v
                       Row 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An index scan is most useful when the query returns a small, selective portion of the table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Index-only scans
&lt;/h3&gt;

&lt;p&gt;If an index contains every value required by a query, PostgreSQL may use an &lt;code&gt;Index Only Scan&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_users_email_name&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&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 index could support:&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;email&lt;/span&gt;&lt;span class="p"&gt;,&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;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'alice@example.com'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL may still need to check visibility information from the table, so an index-only scan does not always mean that the table is completely ignored.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bitmap scan
&lt;/h2&gt;

&lt;p&gt;A bitmap scan is useful when PostgreSQL expects more than a few matches, but not enough to justify scanning the entire table.&lt;/p&gt;

&lt;p&gt;It normally appears as two operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bitmap Heap Scan on users
  -&amp;gt; Bitmap Index Scan on idx_users_status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL first uses the index to collect matching tuple locations. It then groups those locations by table page before fetching the records.&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%2Fgzso694rupn021s8zdrt.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%2Fgzso694rupn021s8zdrt.png" alt="A bitmap scan groups matching tuple locations by page before fetching table data, reducing scattered page access" width="800" height="868"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This can reduce scattered access by allowing PostgreSQL to fetch each required page and retrieve multiple matching rows from it.&lt;/p&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scan type&lt;/th&gt;
&lt;th&gt;Usually useful when&lt;/th&gt;
&lt;th&gt;Main trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sequential scan&lt;/td&gt;
&lt;td&gt;A large part of the table is needed&lt;/td&gt;
&lt;td&gt;Inspects many rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Index scan&lt;/td&gt;
&lt;td&gt;A small number of rows is needed&lt;/td&gt;
&lt;td&gt;May perform random page access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bitmap scan&lt;/td&gt;
&lt;td&gt;A moderate number of rows is needed&lt;/td&gt;
&lt;td&gt;Must build and process a bitmap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Index-only scan&lt;/td&gt;
&lt;td&gt;Required values are covered by an index&lt;/td&gt;
&lt;td&gt;May still require visibility checks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fotmlakpeq3bcr98krsv8.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%2Fotmlakpeq3bcr98krsv8.png" alt="A simplified scan-selection model. PostgreSQL chooses the plan with the lowest estimated cost—not the plan with the most indexes" width="800" height="245"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are guidelines, not fixed rules. PostgreSQL chooses the plan with the lowest estimated cost.&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%2Fixpuqdstart2k3wdrbxn.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%2Fixpuqdstart2k3wdrbxn.png" alt="Low cortisol meme" width="800" height="552"&gt;&lt;/a&gt;&lt;/p&gt;






&lt;h2&gt;
  
  
  Why schema design matters
&lt;/h2&gt;

&lt;p&gt;Schema design affects more than data organization. It can influence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Row width&lt;/li&gt;
&lt;li&gt;The number of pages required&lt;/li&gt;
&lt;li&gt;Cache efficiency&lt;/li&gt;
&lt;li&gt;Query memory usage&lt;/li&gt;
&lt;li&gt;Network traffic&lt;/li&gt;
&lt;li&gt;Serialization and deserialization work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consider a table containing several large fields:&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;orders&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;customer_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&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;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;customer_notes&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;internal_notes&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;provider_response&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An order-list endpoint probably does not need every field.&lt;/p&gt;

&lt;p&gt;Instead of:&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;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Request only the columns the application needs:&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Selecting fewer columns can reduce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query output width&lt;/li&gt;
&lt;li&gt;Network transfer&lt;/li&gt;
&lt;li&gt;Application memory usage&lt;/li&gt;
&lt;li&gt;Serialization work&lt;/li&gt;
&lt;li&gt;Retrieval of unnecessary large values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It may also make an index-only scan possible when the required columns are covered by an index.&lt;/p&gt;

&lt;p&gt;PostgreSQL has optimizations for null and large values, including variable-length storage and TOAST. Adding a column therefore does not always increase every row by its full declared size.&lt;/p&gt;

&lt;p&gt;Still, columns—especially large text and JSON fields—should be added deliberately.&lt;/p&gt;




&lt;h2&gt;
  
  
  Indexes are powerful, but not free
&lt;/h2&gt;

&lt;p&gt;Columns frequently used for filtering, joining, or sorting are often good index candidates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_customer_id&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Composite indexes can support more specific access patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_customer_created&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This may improve:&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Benefits of indexes
&lt;/h3&gt;

&lt;p&gt;Indexes can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoid unnecessary table scans.&lt;/li&gt;
&lt;li&gt;Improve selective lookups.&lt;/li&gt;
&lt;li&gt;Speed up joins.&lt;/li&gt;
&lt;li&gt;Support sorting.&lt;/li&gt;
&lt;li&gt;Enforce uniqueness.&lt;/li&gt;
&lt;li&gt;Enable index-only scans.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Costs of indexes
&lt;/h3&gt;

&lt;p&gt;Indexes also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consume storage.&lt;/li&gt;
&lt;li&gt;Make inserts more expensive.&lt;/li&gt;
&lt;li&gt;Add work to updates and deletes.&lt;/li&gt;
&lt;li&gt;Require vacuuming and maintenance.&lt;/li&gt;
&lt;li&gt;Can become bloated or redundant.&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxl5mqbtmjo4cc5pbwhu0.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%2Fxl5mqbtmjo4cc5pbwhu0.png" alt="Every maintained index may add work to inserts, updates, deletes, vacuuming, and storage" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;Indexes trade storage and write performance for faster reads. The goal is not to index every column, but to create a small set of useful indexes based on real query patterns.&lt;/p&gt;

&lt;p&gt;A frequently queried column is not necessarily a good index candidate if most rows contain the same value.&lt;/p&gt;




&lt;h2&gt;
  
  
  Understanding queries with &lt;code&gt;EXPLAIN&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The most useful tool during my investigation was:&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;VERBOSE&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'alice@example.com'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each option provides different information:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;EXPLAIN&lt;/code&gt; displays the selected execution plan.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ANALYZE&lt;/code&gt; executes the query and reports actual measurements.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BUFFERS&lt;/code&gt; reports how PostgreSQL accessed data pages.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;VERBOSE&lt;/code&gt; includes additional details about operations and output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Be careful: &lt;code&gt;ANALYZE&lt;/code&gt; actually executes the statement.&lt;/p&gt;

&lt;p&gt;For a modifying query, investigate it inside a transaction and roll it back:&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;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;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'inactive'&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;h2&gt;
  
  
  Reading an execution plan
&lt;/h2&gt;

&lt;p&gt;A simplified plan might look 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%2Fq15tadt856afw11rkq2e.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%2Fq15tadt856afw11rkq2e.png" alt="The main fields to inspect in a PostgreSQL execution plan" width="799" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Buffers
&lt;/h3&gt;

&lt;p&gt;A plan may include:&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=12 read=4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At a high level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;shared hit&lt;/code&gt; means the block was already in PostgreSQL’s shared buffers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;shared read&lt;/code&gt; means the block had to be loaded into shared buffers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dirtied&lt;/code&gt; means the block was modified in memory.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;written&lt;/code&gt; means a block was written out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Buffer information adds useful context because execution times can change depending on caching.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sequential scan versus index scan
&lt;/h2&gt;

&lt;p&gt;Before adding an index, a selective email lookup might produce:&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 users
  Filter: (email = 'target@example.com')
  Rows Removed by Filter: 999999
  Buffers: shared hit=2100 read=6200
Execution Time: 82.400 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After creating an 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_users_email&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&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;users&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The plan might change to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Index Scan using idx_users_email on users
  Index Cond: (email = 'target@example.com')
  Buffers: shared hit=4
Execution Time: 0.080 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact numbers depend on the data, caching, hardware, and configuration. The important difference is the amount of work performed.&lt;/p&gt;

&lt;p&gt;The index is not magically making PostgreSQL faster. It is allowing PostgreSQL to &lt;strong&gt;do less work&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  A practical query-investigation workflow
&lt;/h2&gt;

&lt;p&gt;When investigating a slow query:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;EXPLAIN (ANALYZE, BUFFERS, VERBOSE)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Check the scan type.&lt;/li&gt;
&lt;li&gt;Compare estimated and actual rows.&lt;/li&gt;
&lt;li&gt;Inspect buffer usage.&lt;/li&gt;
&lt;li&gt;Look for rows removed by filters.&lt;/li&gt;
&lt;li&gt;Check how many times each node loops.&lt;/li&gt;
&lt;li&gt;Remove unnecessary selected columns.&lt;/li&gt;
&lt;li&gt;Review existing indexes.&lt;/li&gt;
&lt;li&gt;Test using realistic data.&lt;/li&gt;
&lt;li&gt;Make one change and measure again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Performance optimization should be evidence-driven:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Measure → Change → Measure again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Guess → Add indexes → Deploy → Hope
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fytls4vwa9my50xgvxeaa.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%2Fytls4vwa9my50xgvxeaa.png" alt="PostgreSQL performance is primarily about reducing unnecessary work while balancing read, write, memory, and storage costs" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Helpful PostgreSQL metadata queries
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Describe a table in &lt;code&gt;psql&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;\d public.users
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For additional details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;\d+ public.users
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are &lt;code&gt;psql&lt;/code&gt; commands and may not work in every database client.&lt;/p&gt;

&lt;h3&gt;
  
  
  List columns and their types
&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;ordinal_position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;column_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;is_nullable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;column_default&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;table_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'users'&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;ordinal_position&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  List table indexes
&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;indexname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;indexdef&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_indexes&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;tablename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'users'&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;indexname&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check table and index sizes
&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;pg_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;pg_table_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'public.users'&lt;/span&gt;&lt;span class="p"&gt;)&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;table_size&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_indexes_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'public.users'&lt;/span&gt;&lt;span class="p"&gt;)&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_size_pretty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;pg_total_relation_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'public.users'&lt;/span&gt;&lt;span class="p"&gt;)&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;total_size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check scan statistics
&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;relname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;seq_scan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;seq_tup_read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;idx_scan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;idx_tup_fetch&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_dead_tup&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;schemaname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'public'&lt;/span&gt;
  &lt;span class="k"&gt;AND&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;'users'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are cumulative statistics. A high sequential-scan count does not automatically indicate a problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Understanding how PostgreSQL executes a query changed the way I approach database performance.&lt;/p&gt;

&lt;p&gt;The main lesson is simple: &lt;strong&gt;performance is often about reducing unnecessary work.&lt;/strong&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Returning only the required columns&lt;/li&gt;
&lt;li&gt;Examining fewer rows&lt;/li&gt;
&lt;li&gt;Reading fewer pages&lt;/li&gt;
&lt;li&gt;Creating indexes for real access patterns&lt;/li&gt;
&lt;li&gt;Keeping planner statistics current&lt;/li&gt;
&lt;li&gt;Balancing read improvements against write costs&lt;/li&gt;
&lt;li&gt;Measuring changes instead of guessing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly, use:&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;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It turns query optimization from guesswork into investigation.&lt;/p&gt;

&lt;p&gt;Once you can read an execution plan, PostgreSQL stops feeling like a black box. You can see the decisions it makes—and begin designing systems that work with the database rather than against it.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;&lt;em&gt;Hopefully reading through my first post was not an odyssey and if you made it this far then your answer to the below question might be yes ;)&lt;/em&gt;&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%2Fwdfrg0vkg52i4viankex.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%2Fwdfrg0vkg52i4viankex.png" alt="will smith i robot meme" width="800" height="1224"&gt;&lt;/a&gt;&lt;/p&gt;

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