<?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: Saqib Ameen Subhan</title>
    <description>The latest articles on DEV Community by Saqib Ameen Subhan (@saqibameen86).</description>
    <link>https://dev.to/saqibameen86</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%2F4049802%2F2d78cd6c-e644-4178-86b6-6a4638bfe5a4.jpg</url>
      <title>DEV Community: Saqib Ameen Subhan</title>
      <link>https://dev.to/saqibameen86</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saqibameen86"/>
    <language>en</language>
    <item>
      <title>In Cassandra, a delete is a write (and reads pay for it)</title>
      <dc:creator>Saqib Ameen Subhan</dc:creator>
      <pubDate>Sun, 23 Aug 2026 16:36:09 +0000</pubDate>
      <link>https://dev.to/saqibameen86/in-cassandra-a-delete-is-a-write-and-reads-pay-for-it-5mb</link>
      <guid>https://dev.to/saqibameen86/in-cassandra-a-delete-is-a-write-and-reads-pay-for-it-5mb</guid>
      <description>&lt;p&gt;In Cassandra, a delete is a write.&lt;/p&gt;

&lt;p&gt;That sentence sounds like trivia until it takes down a read path. &lt;code&gt;DELETE&lt;/code&gt; doesn't remove anything. It &lt;em&gt;writes&lt;/em&gt; a &lt;strong&gt;tombstone&lt;/strong&gt;, a marker saying "this data is dead as of timestamp T." The actual removal happens later, during compaction, and only after a grace period. Between those two moments, your deleted data has negative value. It's gone from your application's point of view but still physically present, and every read that touches its range must process it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the marker has to exist
&lt;/h2&gt;

&lt;p&gt;Cassandra is a distributed, eventually consistent system. Suppose a delete simply removed data, and one replica was down when the delete happened. When it comes back, it still has the row, and to the rest of the cluster, that looks like &lt;em&gt;data the others are missing&lt;/em&gt;. Repair would helpfully copy the deleted row back to everyone.&lt;/p&gt;

&lt;p&gt;Deleted data returning from the dead is called &lt;strong&gt;zombie data&lt;/strong&gt;, and tombstones exist to prevent it. The tombstone outranks the older value everywhere it's seen.&lt;/p&gt;

&lt;p&gt;Which is why tombstones must survive for &lt;code&gt;gc_grace_seconds&lt;/code&gt;, &lt;strong&gt;default 864000, ten days&lt;/strong&gt;, before compaction may purge them. Ten days is the window you have to repair a down replica. Shrink &lt;code&gt;gc_grace_seconds&lt;/code&gt; without shrinking your repair interval and you've quietly signed up for zombies. Repair at scale is its own post, and it's coming.&lt;/p&gt;

&lt;h2&gt;
  
  
  How reads pay
&lt;/h2&gt;

&lt;p&gt;A read merges data across memtable and SSTables. Every tombstone in the requested range must be read, held, and reconciled against live data before Cassandra can answer. A partition that has accumulated a million tombstones makes you &lt;em&gt;process a million dead cells to return whatever's alive&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Cassandra tells you when this is happening, in two escalating tones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WARN  ReadCommand - Read 812 live rows and 104,832 tombstone cells for query ...
ERROR ... Scanned over 100001 tombstones ... query aborted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The warning fires past &lt;code&gt;tombstone_warn_threshold&lt;/code&gt;, 1,000 by default. Past &lt;code&gt;tombstone_failure_threshold&lt;/code&gt;, 100,000, the read is killed mid flight with &lt;code&gt;TombstoneOverwhelmingException&lt;/code&gt;. That's the database choosing to fail your query rather than let it OOM the node.&lt;/p&gt;

&lt;p&gt;The thresholds are guardrails, not tuning knobs. Raising them treats the symptom and keeps the disease.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where tombstone farms come from
&lt;/h2&gt;

&lt;p&gt;Every one of these I've met in production.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Queue like workloads.&lt;/strong&gt; Insert, process, delete, repeat, in the same partition. The partition becomes a graveyard the consumer must scan past on every poll. Cassandra as a queue is the canonical anti pattern for exactly this reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collection overwrites.&lt;/strong&gt; &lt;code&gt;UPDATE t SET mymap = {...}&lt;/code&gt; replaces a whole collection, which writes a range tombstone over the old one first. Prefer additive updates, &lt;code&gt;mymap = mymap + {...}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inserting NULLs.&lt;/strong&gt; Binding null in a prepared statement writes a tombstone for that cell. ORMs and "just bind every column" code generate these invisibly. Use unset values, not nulls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTL everywhere.&lt;/strong&gt; Expired TTL cells become tombstones too. Fine &lt;em&gt;if&lt;/em&gt; your compaction strategy is built for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Finding them, then fixing them
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# per-SSTable estimate&lt;/span&gt;
sstablemetadata /var/lib/cassandra/data/ks/table-&lt;span class="k"&gt;*&lt;/span&gt;/ &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="nt"&gt;-Data&lt;/span&gt;.db | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; droppable
&lt;span class="c"&gt;# Estimated droppable tombstones: 0.83&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;0.83 means an estimated 83% of that SSTable is purgeable tombstones.&lt;/p&gt;

&lt;p&gt;For query level visibility, &lt;code&gt;TRACING ON&lt;/code&gt; in cqlsh shows tombstone cells scanned per query. For a live table, &lt;code&gt;nodetool tablestats&lt;/code&gt; and watch average tombstones per slice.&lt;/p&gt;

&lt;p&gt;The durable fixes are modeling fixes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Time series with TTL, use TimeWindowCompactionStrategy.&lt;/strong&gt; Whole SSTables age out and get dropped as files, so tombstone processing largely disappears. Compaction strategy trade offs are the next post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop deleting in place in hot partitions.&lt;/strong&gt; Partition by time bucket and let whole partitions expire instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit for null binding and collection overwrites.&lt;/strong&gt; These are the tombstones nobody meant to write.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Deletes in Cassandra are cheap to issue and expensive to have issued. Model as if every delete is a small loan against your read latency, because it is, and &lt;code&gt;gc_grace_seconds&lt;/code&gt; is the repayment schedule.&lt;/p&gt;

</description>
      <category>cassandra</category>
      <category>database</category>
      <category>performance</category>
      <category>distributed</category>
    </item>
    <item>
      <title>How to delete 2TB from a live MongoDB cluster without anyone noticing</title>
      <dc:creator>Saqib Ameen Subhan</dc:creator>
      <pubDate>Sun, 23 Aug 2026 16:04:47 +0000</pubDate>
      <link>https://dev.to/saqibameen86/how-to-delete-2tb-from-a-live-mongodb-cluster-without-anyone-noticing-1pe2</link>
      <guid>https://dev.to/saqibameen86/how-to-delete-2tb-from-a-live-mongodb-cluster-without-anyone-noticing-1pe2</guid>
      <description>&lt;p&gt;The request was simple. "There's about 2TB of expired data in this collection. Can you delete it tonight?"&lt;/p&gt;

&lt;p&gt;The honest answer was no, and being able to explain &lt;em&gt;why&lt;/em&gt;, not just refuse, is most of the job. A missing TTL index had let short lived data pile up for months in one of our busiest collections. The team wanted one big &lt;code&gt;deleteMany&lt;/code&gt; overnight. Here's what that would have actually done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a mass delete hurts, and it isn't locking
&lt;/h2&gt;

&lt;p&gt;People reach for "locks" as the explanation. Wrong database era. WiredTiger uses document level concurrency, so a huge delete doesn't freeze the collection. The damage arrives through three quieter channels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The oplog becomes a firehose.&lt;/strong&gt; Every deleted document is an individual entry in the oplog. Delete 500 million documents and you've written 500 million oplog entries, which every secondary must pull and apply. Replication lag climbs. If lag exceeds what the oplog window can hold, a secondary falls off the oplog entirely and needs a &lt;strong&gt;full resync&lt;/strong&gt;. Now your delete has cost you redundancy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Cache churn.&lt;/strong&gt; To delete a document, WiredTiger loads its pages into cache. A 2TB sweep drags cold data through a cache that was carefully full of hot data. Your working set gets evicted, p99 latency on unrelated queries climbs, and someone opens an incident that will never mention the word "delete."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Acknowledgment pressure.&lt;/strong&gt; If your writes use &lt;code&gt;w:"majority"&lt;/code&gt; (they should, see the write concern post), the delete's progress is gated on those same struggling secondaries. Everything compounds.&lt;/p&gt;

&lt;p&gt;I demonstrated exactly this in a lower environment. Kicked off the naive delete, watched &lt;code&gt;rs.printSecondaryReplicationInfo()&lt;/code&gt; lag grow, and the "tonight" deadline converted itself into a plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern: chunk, sleep, watch the lag
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CHUNK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SLEEP_MS&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ISODate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-01-01&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;total&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;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$lt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cutoff&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                              &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;_id&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="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CHUNK&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SLEEP_MS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// let the oplog breathe&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;1000000&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; deleted, lag check...`&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;Two operational rules around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run in off peak windows only.&lt;/strong&gt; We ran nightly windows and paused at business hours. Duration was about a week and a half for the full 2TB. Nobody noticed, which was the definition of success.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch replication lag as your throttle.&lt;/strong&gt; If lag climbs past your comfort line, raise &lt;code&gt;SLEEP_MS&lt;/code&gt;. The loop's speed limit is the cluster's health, not your patience.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sleep()&lt;/code&gt; between chunks looks unbearably conservative to developers. That is the point. The delete has no deadline once the growth is stopped, and the growth is stopped by the &lt;em&gt;real&lt;/em&gt; fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real fix: TTL, so this never becomes a project again
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;created&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expireAfterSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2592000&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;  &lt;span class="c1"&gt;// 30 days&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Details worth knowing about the TTL monitor, because they surprise people.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It wakes &lt;strong&gt;every 60 seconds&lt;/strong&gt;, so expiry is approximate, not instant. Fine for cleanup, wrong tool for business logic that needs precise expiry.&lt;/li&gt;
&lt;li&gt;It deletes in &lt;strong&gt;background batches&lt;/strong&gt;, effectively running my chunked loop forever, gently. On huge backlogs it can lag, which is why we cleared the 2TB manually first and let TTL own steady state.&lt;/li&gt;
&lt;li&gt;Index the field the data actually ages by. TTL on the wrong date field is a slow motion data loss incident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We closed it with a runbook, the chunk script, the lag thresholds and the window schedule, so the next 2TB request is a lookup, not a debate.&lt;/p&gt;

&lt;p&gt;The fastest way to delete 2TB is slowly.&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>operations</category>
      <category>performance</category>
      <category>database</category>
    </item>
    <item>
      <title>I take down a healthy primary on purpose. You should too.</title>
      <dc:creator>Saqib Ameen Subhan</dc:creator>
      <pubDate>Sun, 23 Aug 2026 15:58:41 +0000</pubDate>
      <link>https://dev.to/saqibameen86/i-take-down-a-healthy-primary-on-purpose-you-should-too-bm7</link>
      <guid>https://dev.to/saqibameen86/i-take-down-a-healthy-primary-on-purpose-you-should-too-bm7</guid>
      <description>&lt;p&gt;At one company I built an autoscaling pipeline that replaced MongoDB cluster nodes automatically. CloudWatch caught the CPU threshold, and the automation rolled the cluster node by node. Secondary first, second secondary next, primary last. Twelve minutes, no human, no application impact.&lt;/p&gt;

&lt;p&gt;The order is the interesting part. Rolling the primary last means the cluster experiences exactly &lt;strong&gt;one&lt;/strong&gt; election per cycle, at a moment the automation chooses. Which only works if you trust elections. And you only get to trust elections by triggering them yourself, regularly, and watching what your application does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happens in an election
&lt;/h2&gt;

&lt;p&gt;The mechanics, compressed.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every node heartbeats every 2 seconds. A node that misses heartbeats for &lt;code&gt;electionTimeoutMillis&lt;/code&gt; (default &lt;strong&gt;10 seconds&lt;/strong&gt;) is presumed gone.&lt;/li&gt;
&lt;li&gt;An electable secondary calls an election. Voting is not seniority. A node only wins if its oplog is at least as recent as each voter's. &lt;strong&gt;Freshness outranks priority.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;priority&lt;/code&gt; settings bias who &lt;em&gt;stands&lt;/em&gt; for election, not who deserves to win. A priority 10 node with a stale oplog loses to a priority 1 node that's caught up.&lt;/li&gt;
&lt;li&gt;Majority vote, new primary, secondaries sync from it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Wall clock cost is typically a few seconds for the election itself, bounded by that 10 second detection window on the front. Call it 5 to 15 seconds of no primary, during which writes cannot be acknowledged.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that pages you isn't the election
&lt;/h2&gt;

&lt;p&gt;It's what your application does during those seconds. Three behaviours, in ascending order of maturity.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Errors bubble to users.&lt;/strong&gt; Driver throws &lt;code&gt;NotWritablePrimary&lt;/code&gt;, nobody catches it, checkout fails.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blind retry loops.&lt;/strong&gt; Sometimes double applies the write. Worse than failing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;retryWrites=true&lt;/code&gt; plus a sane &lt;code&gt;serverSelectionTimeoutMS&lt;/code&gt;.&lt;/strong&gt; The driver holds the write, discovers the new primary from the topology update, retries exactly once. To the user, one slow request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not find out which of these you are during a real incident. That's the wrong time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The drill
&lt;/h2&gt;

&lt;p&gt;Quarterly. In staging first, then production during a low traffic window. Yes, production. Staging tells you the drill works, production tells you the truth.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. Note current topology&lt;/span&gt;
&lt;span class="nx"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stateStr&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Ask the primary to step down gracefully&lt;/span&gt;
&lt;span class="nx"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stepDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// refuses re-election for 60s&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Watch the election from a secondary&lt;/span&gt;
&lt;span class="nx"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While it runs, the numbers I capture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;detection to new primary elected : ___ s
application error count          : ___
p99 latency during window        : ___ ms
first write after failover       : double applied? (check by unique key)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those blanks are the deliverable. Fill them from your own drill.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rs.stepDown()&lt;/code&gt; is the polite version. The primary flushes and closes cleanly. Once that's boring, graduate to the impolite version. &lt;code&gt;kill -9&lt;/code&gt; the primary's &lt;code&gt;mongod&lt;/code&gt;, or drop its network. That's the one that resembles an actual cloud incident, and it exercises the full 10 second detection window instead of skipping it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two configuration notes from doing this at scale
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Even numbered voting members are a bug.&lt;/strong&gt; Four voters can split 2 to 2 and elect nobody. Keep voting members odd. Use an arbiter only if you truly can't afford a third data node, and know that arbiters don't help &lt;code&gt;w:"majority"&lt;/code&gt; acknowledge anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Priorities encode your topology intent.&lt;/strong&gt; In multi region clusters we set higher priorities in the primary region so failback happens automatically once nodes recover. But again, priority only nominates. The oplog decides.&lt;/p&gt;

&lt;p&gt;An election you've rehearsed is an operational event. An election you haven't is an outage with paperwork.&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>highavailability</category>
      <category>sre</category>
      <category>database</category>
    </item>
    <item>
      <title>"The write was acknowledged" means less than you think</title>
      <dc:creator>Saqib Ameen Subhan</dc:creator>
      <pubDate>Sun, 23 Aug 2026 15:49:20 +0000</pubDate>
      <link>https://dev.to/saqibameen86/the-write-was-acknowledged-means-less-than-you-think-4nm8</link>
      <guid>https://dev.to/saqibameen86/the-write-was-acknowledged-means-less-than-you-think-4nm8</guid>
      <description>&lt;p&gt;I spent years operating databases for payment platforms, where a lost write isn't a bug, it's a regulatory conversation. That environment teaches you to read the phrase "the write was acknowledged" the way a lawyer reads a contract. Acknowledged &lt;em&gt;by whom&lt;/em&gt;? Durable &lt;em&gt;against what&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;In MongoDB, that one sentence has at least four different meanings, controlled by two settings most applications leave at defaults without ever deciding to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four contracts
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. w:1, the primary has it in memory&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;writeConcern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;w&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="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// 2. w:1, j:true, the primary has it in its journal on disk&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;writeConcern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;w&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="na"&gt;j&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// 3. w:"majority", a majority of the replica set has it&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;writeConcern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;w&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;majority&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// 4. w:"majority", j:true, a majority has it journaled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each level survives a different failure.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;w:1&lt;/strong&gt; survives nothing interesting. If the primary's &lt;code&gt;mongod&lt;/code&gt; crashes before the next journal flush, the write is gone, while your application already told the user "success."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;w:1, j:true&lt;/strong&gt; survives a process crash on the primary. It does not survive the primary &lt;em&gt;failing over&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;w:"majority"&lt;/strong&gt; survives failover. This is the one that matters, and here's why.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where acknowledged writes go to die
&lt;/h2&gt;

&lt;p&gt;A replica set election isn't polite. If the primary accepts writes at &lt;code&gt;w:1&lt;/code&gt;, then loses connectivity before replicating them, a secondary gets elected and moves on. When the old primary rejoins, it discovers history diverged. It has writes the new primary never saw.&lt;/p&gt;

&lt;p&gt;Those writes get &lt;strong&gt;rolled back&lt;/strong&gt;. MongoDB doesn't delete them silently, it writes them to BSON files in the &lt;code&gt;rollback&lt;/code&gt; directory, where, in my experience, they are examined by precisely no one until an auditor asks a question.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;w:"majority"&lt;/code&gt; closes this hole. The write isn't acknowledged until enough nodes have it that &lt;em&gt;any electable primary&lt;/em&gt; must have it too. It cannot be elected away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The read side of the same contract
&lt;/h2&gt;

&lt;p&gt;Durable writes with careless reads is half a system. Two settings complete it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Don't show me data that could still be rolled back&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({...}).&lt;/span&gt;&lt;span class="nf"&gt;readConcern&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;majority&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// My own session should see its own writes, in order&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startSession&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;causalConsistency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And on the driver, &lt;code&gt;retryWrites=true&lt;/code&gt; (default in modern drivers) makes the failover window survivable. A write interrupted by an election is retried exactly once against the new primary, safely, because every write carries a unique transaction number.&lt;/p&gt;

&lt;p&gt;Which is also why "just wrap it in a retry loop yourself" is worse than the built in one. Your loop can double apply. The driver's cannot.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually deploy
&lt;/h2&gt;

&lt;p&gt;Tiering, not one global setting.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data&lt;/th&gt;
&lt;th&gt;Write concern&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Payments, ledger, anything auditable&lt;/td&gt;
&lt;td&gt;&lt;code&gt;w:"majority"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rollback is not an acceptable word here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User profile updates&lt;/td&gt;
&lt;td&gt;&lt;code&gt;w:"majority"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cheap insurance, users notice lost edits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High volume telemetry, click events&lt;/td&gt;
&lt;td&gt;&lt;code&gt;w:1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Losing two seconds of events in a rare failover is a fair trade for throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The latency cost of majority is one replication round trip, single digit milliseconds inside a region. I've watched teams run payments at &lt;code&gt;w:1&lt;/code&gt; to save five milliseconds, which is a way of saying they priced a lost financial record at five milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lab
&lt;/h2&gt;

&lt;p&gt;Start a 3 node replica set locally (&lt;code&gt;mongod --replSet&lt;/code&gt; three times, or docker), then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// terminal 1: insert at w:1 in a loop&lt;/span&gt;
&lt;span class="c1"&gt;// terminal 2: rs.stepDown() on the primary mid-loop&lt;/span&gt;
&lt;span class="c1"&gt;// then check the old primary's rollback directory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /data/rs1/rollback/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it. Seeing your own acknowledged write sitting in a rollback file is worth more than this whole post.&lt;/p&gt;

&lt;p&gt;Defaults are decisions someone else made. For write concern, make your own, per collection, on purpose, written down.&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>database</category>
      <category>reliability</category>
      <category>distributed</category>
    </item>
    <item>
      <title>cannot index parallel arrays, the MongoDB error that improves your schema</title>
      <dc:creator>Saqib Ameen Subhan</dc:creator>
      <pubDate>Sun, 16 Aug 2026 21:41:53 +0000</pubDate>
      <link>https://dev.to/saqibameen86/cannot-index-parallel-arrays-the-mongodb-error-that-improves-your-schema-4pp</link>
      <guid>https://dev.to/saqibameen86/cannot-index-parallel-arrays-the-mongodb-error-that-improves-your-schema-4pp</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MongoServerError: cannot index parallel arrays [certifications] [skills]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've never hit this error, your schemas have been lucky. A team once brought me a document shaped like this during a design review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;_id&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;engineer-42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;skills&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mongodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cassandra&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;python&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;certifications&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dba&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;atlas-admin&lt;/span&gt;&lt;span class="dl"&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;They wanted fast queries on skill and certification combinations, so the natural move was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engineers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;skills&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="na"&gt;certifications&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="c1"&gt;// MongoServerError: cannot index parallel arrays&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MongoDB refuses. Not "performs badly", refuses. And the reason is one of those internals that, once you see it, changes how you model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why: multikey indexes are per element
&lt;/h2&gt;

&lt;p&gt;When you index an array field, MongoDB creates a &lt;strong&gt;multikey index&lt;/strong&gt;, one index entry per array element. Index &lt;code&gt;skills&lt;/code&gt; on the document above and you get three entries, one for &lt;code&gt;"mongodb"&lt;/code&gt;, one for &lt;code&gt;"cassandra"&lt;/code&gt;, one for &lt;code&gt;"python"&lt;/code&gt;, each pointing at the same document.&lt;/p&gt;

&lt;p&gt;Now imagine a compound index across two independent arrays. To answer any combination query, MongoDB would need an entry for every element of the &lt;em&gt;cross product&lt;/em&gt;. Three skills times two certifications is six entries. Harmless here. But two arrays of 1,000 elements each is a &lt;strong&gt;million index entries for a single document&lt;/strong&gt;. One &lt;code&gt;insertOne&lt;/code&gt; fans out into a million B-tree writes. The server declines to let you build that footgun, and honestly, good.&lt;/p&gt;

&lt;p&gt;There's a quieter problem hiding in the schema too. The combinations are meaningless. Nothing in two parallel arrays says &lt;em&gt;which&lt;/em&gt; skill relates to &lt;em&gt;which&lt;/em&gt; certification. The data model can't answer the question the index was supposed to serve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: one array of subdocuments
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;_id&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;engineer-42&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;quals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;skill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mongodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dba&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;skill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mongodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="na"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;atlas-admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;skill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cassandra&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engineers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;quals.skill&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;quals.cert&lt;/span&gt;&lt;span class="dl"&gt;"&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="c1"&gt;// works&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is allowed because both indexed paths traverse the &lt;strong&gt;same&lt;/strong&gt; array. Index entries are generated per array element, so each entry is a real &lt;code&gt;(skill, cert)&lt;/code&gt; pair that actually existed together in the data, the correlation the parallel arrays destroyed. The index gets smaller and the model gets more truthful. That combination is rare.&lt;/p&gt;

&lt;p&gt;One caveat worth knowing before you rely on it. To match both fields &lt;em&gt;within the same element&lt;/em&gt;, query with &lt;code&gt;$elemMatch&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engineers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;quals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$elemMatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;skill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mongodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dba&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&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;Without &lt;code&gt;$elemMatch&lt;/code&gt;, MongoDB matches documents where &lt;em&gt;some&lt;/em&gt; element has the skill and &lt;em&gt;some other&lt;/em&gt; element has the cert. The parallel arrays bug, reincarnated at query time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lab
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&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;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;a&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="na"&gt;b&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="c1"&gt;// watch it fail&lt;/span&gt;

&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="na"&gt;a&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="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},{&lt;/span&gt;&lt;span class="na"&gt;a&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="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pairs.a&lt;/span&gt;&lt;span class="dl"&gt;"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pairs.b&lt;/span&gt;&lt;span class="dl"&gt;"&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="c1"&gt;// watch it work&lt;/span&gt;

&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$elemMatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;a&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="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;   &lt;span class="c1"&gt;// zero results, and that's correct&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line is the whole lesson in one query. &lt;code&gt;a:1&lt;/code&gt; and &lt;code&gt;b:"y"&lt;/code&gt; never occurred in the same element, so the document doesn't match. Parallel arrays would have happily lied to you.&lt;/p&gt;

&lt;p&gt;Some errors are the database being difficult. This one is the database refusing to let your schema make a promise it can't keep.&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>indexing</category>
      <category>datamodeling</category>
      <category>database</category>
    </item>
    <item>
      <title>The explain plan I read before the CEO called back!</title>
      <dc:creator>Saqib Ameen Subhan</dc:creator>
      <pubDate>Tue, 04 Aug 2026 05:09:05 +0000</pubDate>
      <link>https://dev.to/saqibameen86/the-explain-plan-i-read-before-the-ceo-called-back-1kla</link>
      <guid>https://dev.to/saqibameen86/the-explain-plan-i-read-before-the-ceo-called-back-1kla</guid>
      <description>&lt;p&gt;The escalation reached our CEO before it reached my team.&lt;/p&gt;

&lt;p&gt;An enterprise customer, one of the largest on the platform, was seeing latency spikes bad enough that they raised it commercially, not technically. I had about twenty minutes before a call with their leadership, so I did the only thing that ever works in that situation: I stopped guessing and pulled the profiler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setProfilingLevel&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slowms&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="c1"&gt;// ...wait one cycle...&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;millis&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="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I set the threshold (1000 ms) high on purpose. The default log already captures slow queries over 100 ms, but the profiler gives me something the log doesn't: structured records I can sort and aggregate. I want it catching only the genuinely pathological queries, so the culprit stands out and the profiler itself stays light on an already stressed system.&lt;/p&gt;

&lt;p&gt;One aggregation stood out. It ran every five minutes, scanned a collection of roughly 200 million documents, and had no index supporting it. Every five minutes, a full collection scan, on shared infrastructure. The "noisy neighbor" wasn't a mystery, it was on a schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I actually read an explain plan
&lt;/h2&gt;

&lt;p&gt;Run the slow query with execution stats:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;explain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;executionStats&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ACME&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$gte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ISODate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-08-01&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;created&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="p"&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;I look at exactly three numbers before anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nReturned&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;214&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;totalKeysExamined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;totalDocsExamined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;198236411&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ratio is the diagnosis. Healthy queries examine roughly as many keys as they return. This one examined 198 million documents to return 214. &lt;code&gt;totalKeysExamined: 0&lt;/code&gt; means no index was touched at all. The winning plan was &lt;code&gt;COLLSCAN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the whole skill, honestly. Everything else in the explain output is detail. If &lt;code&gt;totalDocsExamined&lt;/code&gt; is orders of magnitude above &lt;code&gt;nReturned&lt;/code&gt;, the query is doing the database's job by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  ESR: the field order that makes or breaks the index
&lt;/h2&gt;

&lt;p&gt;The fix was not "add an index." It was "add the right index" and the difference is field order. MongoDB compound indexes follow what the docs call the ESR rule, Equality, Sort, Range:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Equality&lt;/strong&gt; predicates first (&lt;code&gt;accountId&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sort&lt;/strong&gt; fields next (&lt;code&gt;created&lt;/code&gt; as sort key)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Range&lt;/strong&gt; predicates last (&lt;code&gt;created: { $gte: ... }&lt;/code&gt;, here sort and range share a field, which is the friendly case)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;accountId&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="na"&gt;status&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="na"&gt;created&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get the order wrong, range before sort, and the index still gets used, but MongoDB has to fetch and sort in memory. You'll see it in the plan as a &lt;code&gt;SORT&lt;/code&gt; stage instead of the sort being absorbed by index order. On 200 million documents, an in-memory sort is not a detail.&lt;/p&gt;

&lt;p&gt;One refinement we used because the query only ever cared about active records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;accountId&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="na"&gt;created&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="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;partialFilterExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&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;A partial index keeps only the entries that match the filter. Smaller index, less RAM, faster writes, and the query planner picks it up as long as the query includes the same predicate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lab, if you want to see it yourself
&lt;/h2&gt;

&lt;p&gt;Five minutes on a local &lt;code&gt;mongod&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;500000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;closed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="na"&gt;created&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;997&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;explain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;executionStats&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A7&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;created&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="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// note totalDocsExamined, then:&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;accountId&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="na"&gt;status&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="na"&gt;created&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="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// run the explain again and compare&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The before and after on &lt;code&gt;totalDocsExamined&lt;/code&gt; makes the argument better than any slide deck.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened with the customer
&lt;/h2&gt;

&lt;p&gt;Root cause explained in plain language on the call. I described the collection as a library with no catalogue, where every request meant walking every aisle. Partial index shipped the same day. Latency resolved within six hours of the escalation, and two weeks later I presented them a health report instead of an apology. They renewed.&lt;/p&gt;

&lt;p&gt;Twenty minutes, the profiler, and three numbers. The hard part was knowing which three.&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>performance</category>
      <category>indexing</category>
      <category>database</category>
    </item>
  </channel>
</rss>
