<?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: Dilip V P</title>
    <description>The latest articles on DEV Community by Dilip V P (@dilip_v_p).</description>
    <link>https://dev.to/dilip_v_p</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%2F1344527%2Ff6c13742-88fe-4d7a-a966-16313ddb2c2f.jpg</url>
      <title>DEV Community: Dilip V P</title>
      <link>https://dev.to/dilip_v_p</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dilip_v_p"/>
    <language>en</language>
    <item>
      <title>How Vector Databases Search a Million Vectors Without Checking a Million</title>
      <dc:creator>Dilip V P</dc:creator>
      <pubDate>Sat, 18 Jul 2026 18:53:10 +0000</pubDate>
      <link>https://dev.to/dilip_v_p/how-vector-databases-search-a-million-vectors-without-checking-a-million-2ee1</link>
      <guid>https://dev.to/dilip_v_p/how-vector-databases-search-a-million-vectors-without-checking-a-million-2ee1</guid>
      <description>&lt;p&gt;Take the word "king." Your database does not store the word. It stores a vector: a list of 768 numbers that place king at a point in space, where words used in similar ways sit nearby.&lt;/p&gt;

&lt;p&gt;That one move changes the whole problem. "Find something similar" becomes "find the nearest point." And finding the nearest point in a space of hundreds of dimensions turns out to be the search your normal database index genuinely cannot do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meaning becomes a place
&lt;/h2&gt;

&lt;p&gt;An embedding model reads enormous amounts of text and learns to place each word (or sentence, or image) at a point, so that things used in similar contexts land near each other. Closeness is usually measured with cosine similarity: how aligned two vectors are, ignoring their length.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentence_transformers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;util&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all-mpnet-base-v2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 768 dimensions
&lt;/span&gt;&lt;span class="n"&gt;vecs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;king&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bicycle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;normalize_embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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="n"&gt;util&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos_sim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vecs&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="n"&gt;vecs&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;# king vs queen   -&amp;gt; ~0.63
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;util&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos_sim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vecs&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="n"&gt;vecs&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="c1"&gt;# king vs bicycle -&amp;gt; ~0.16
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;king lands near queen and far from bicycle. "Similar meaning" is now just "small distance."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why brute force scores every vector
&lt;/h2&gt;

&lt;p&gt;To find the closest vector to a query, the simple approach compares the query to every stored vector and keeps the best:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;all_vectors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="c1"&gt;# N vectors
&lt;/span&gt;    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# normalized vectors, so dot product = cosine
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;best_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is O(N times d). With 768 dimensions, a million vectors is roughly a million dot products for a single search. On my machine a simple CPU scan of 1,000,000 vectors took about 38 ms per search, which extrapolates to a few seconds at 100,000,000. A service doing thousands of searches a second cannot live there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your B-tree index can't save you
&lt;/h2&gt;

&lt;p&gt;The instinct from the earlier posts in this series is "add an index." But a B-tree gives you exactly one sorted order. Sort every vector by dimension 0 and you are blind to the other 767. Two vectors can be neighbors in the full space and sit thousands of positions apart in that single sort.&lt;/p&gt;

&lt;p&gt;In one run, the true nearest word to "coffee" was "starbucks" (similarity 0.69). Sorted by a single dimension, starbucks sat 3,333 positions away from coffee. The closest thing inside a small window of that sort was "meals" at 0.44: wrong, and weaker.&lt;/p&gt;

&lt;p&gt;A composite index does not fix it either. Multiple indexed columns still produce one lexicographic order, not an ordering by distance. Sorted order answers ranges. Similarity is not a range.&lt;/p&gt;

&lt;h2&gt;
  
  
  The small-world idea
&lt;/h2&gt;

&lt;p&gt;Here is the turn. Think of six degrees of separation: most of your friends are local, but a few know someone far away, and those few long-range links let a message cross the world in about six hops.&lt;/p&gt;

&lt;p&gt;Do the same to vectors. Give each one a short list of neighbors, including a few distant ones. Now search is a walk: start somewhere, and keep hopping to whichever neighbor is closer to your query. On a flat graph this already works, but it can crawl, taking many small steps or getting stuck one hop short of the best answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  HNSW: jump, then refine
&lt;/h2&gt;

&lt;p&gt;HNSW (Hierarchical Navigable Small World, Malkov and Yashunin, 2016) fixes the crawl by adding layers, like a skip list for a graph.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top layer: a sparse "airport" map. A few vectors, long jumps across the whole space.&lt;/li&gt;
&lt;li&gt;Middle layers: "highways."&lt;/li&gt;
&lt;li&gt;Bottom layer: every vector, the "streets," for the final precise step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You enter at the top, take big jumps to get roughly there, then drop down a layer and refine, again and again, until the streets put you on the exact neighbor. A single search touches a tiny fraction of the data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hnswlib&lt;/span&gt;

&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hnswlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;space&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cosine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_elements&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vectors&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ef_construction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vectors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_ef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# the accuracy vs speed dial
&lt;/span&gt;&lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;distances&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;knn_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The honest trade
&lt;/h2&gt;

&lt;p&gt;HNSW is approximate. It can miss, and misses are silent, so you tune it against a recall target. &lt;code&gt;ef_search&lt;/code&gt; is the dial: higher looks at more candidates, so recall goes up and speed goes down.&lt;/p&gt;

&lt;p&gt;On my run, on the same 20,000 real embeddings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;brute force, exact: 1.23 ms per search&lt;/li&gt;
&lt;li&gt;HNSW, ef_search = 50: 0.62 ms per search, recall@10 = 99.9%&lt;/li&gt;
&lt;li&gt;HNSW, ef_search = 200: 2.06 ms per search, recall@10 = 100%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice the last line: at 20,000 vectors, cranking ef past a point makes HNSW slower than brute force. Approximate search wins because it scales differently: an exact scan is linear in N, HNSW is roughly logarithmic, so the gap widens as your dataset grows. At a million or a hundred million, brute force is hopeless and HNSW is still fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who runs this
&lt;/h2&gt;

&lt;p&gt;HNSW is one of the most common indexes under semantic search and the retrieval step of AI chatbots:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pgvector (inside Postgres you already run)&lt;/li&gt;
&lt;li&gt;Weaviate (dedicated engine, self-hosted or managed)&lt;/li&gt;
&lt;li&gt;Pinecone (managed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One rule of thumb: start with pgvector in the database you already operate, and graduate to a dedicated engine when RAM, traffic, filtering, or ops actually force you. It is not the only vector index (IVF, PQ, DiskANN, hybrid search all exist), and not every AI search uses it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch it happen
&lt;/h2&gt;

&lt;p&gt;The full episode builds the meaning-map, shows why the B-tree fails, and descends through the HNSW layers:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/4baQ_79vn30"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Numbers are from a demo on my machine, one model everywhere (all-mpnet-base-v2, 768 dims, 20,000 real words). The one-by-one counter is an animation; the per-check cost is measured and the totals are arithmetic. The million and hundred-million scan times are a linear estimate from a simple CPU run, not a universal speed. The HNSW vs brute-force numbers are measured on the 20,000 vectors with hnswlib. Also worth knowing: "similar" here means "used in similar contexts," not synonyms, so opposites like good and bad can score high.&lt;/p&gt;

&lt;p&gt;Which vector database are you using, and what pushed you to it?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>vectordatabase</category>
      <category>machinelearning</category>
      <category>backend</category>
    </item>
    <item>
      <title>The N+1 Query Problem: How One Page Fires 2,101 Queries (and How to Get Back to 3)</title>
      <dc:creator>Dilip V P</dc:creator>
      <pubDate>Sat, 11 Jul 2026 11:08:00 +0000</pubDate>
      <link>https://dev.to/dilip_v_p/the-n1-query-problem-how-one-page-fires-2101-queries-and-how-to-get-back-to-3-3l3i</link>
      <guid>https://dev.to/dilip_v_p/the-n1-query-problem-how-one-page-fires-2101-queries-and-how-to-get-back-to-3-3l3i</guid>
      <description>&lt;p&gt;I put a query counter on a single page and loaded it. The page rendered fine. The counter said 101.&lt;/p&gt;

&lt;p&gt;That's the N+1 problem. And the reason it survives code review, testing, and staging is the twist most explanations bury: every one of those queries is fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the 101 comes from
&lt;/h2&gt;

&lt;p&gt;One innocent loop (pseudo-code, parameters skipped for brevity; never build SQL by string concat in real code):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM posts LIMIT 100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT ... FROM users WHERE id = ?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;author_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One query for the list, plus N queries for the rows. 1 + 100 = 101. Turn on your query log and you can watch it happen: the same SELECT, over and over, one id at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "every query is fast" doesn't save you
&lt;/h2&gt;

&lt;p&gt;Each of those queries is indexed. Each takes about a millisecond. The problem is that every query is a round trip to the database, and a round trip costs a few milliseconds on a real network. Sometimes more.&lt;/p&gt;

&lt;p&gt;The cost of an N+1 is not query speed. It is count times round trip. A hundred round trips at a few milliseconds each, and one page waits half a second.&lt;/p&gt;

&lt;h2&gt;
  
  
  It nests
&lt;/h2&gt;

&lt;p&gt;Each comment also loads its author. Now the loop hides another loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100 posts x 20 comments each
1 (posts) + 100 (authors) + 2,000 (comment authors) = 2,101 queries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is 2,101 queries for one page. Not an estimate, just the arithmetic of a nested loop. N+1 fires one query for every node in your object tree. Comments, authors, tags: add a relation and it multiplies again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: one query per level
&lt;/h2&gt;

&lt;p&gt;Stop asking one row at a time. Collect the ids at each level, fire one batched query per level, and stitch the results in code:&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;posts&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;comments&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;post_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2,101 queries becomes 3. The count now follows the DEPTH of the tree, not the size of it. A million rows? Still 3 queries.&lt;/p&gt;

&lt;p&gt;In most frameworks the batch fix is one line, the eager-load option:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JPA/Hibernate: JOIN FETCH (to-one), @BatchSize or @EntityGraph (to-many)&lt;/li&gt;
&lt;li&gt;Rails: includes / preload&lt;/li&gt;
&lt;li&gt;Django: select_related (to-one) / prefetch_related (to-many)&lt;/li&gt;
&lt;li&gt;GraphQL: DataLoader&lt;/li&gt;
&lt;li&gt;EF Core: Include + AsSplitQuery&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The JOIN trap
&lt;/h2&gt;

&lt;p&gt;"Why not just JOIN everything?" For to-one relations, yes: a post and its one author JOIN into one clean row.&lt;/p&gt;

&lt;p&gt;But JOIN a post to its 50 comments and the post comes back 50 times, once per comment row. Worse, pagination breaks: LIMIT 100 counts joined rows, not posts, so you ask for 100 posts and get 2.&lt;/p&gt;

&lt;p&gt;The rule: to-one, JOIN. To-many, batch with IN.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nobody notices until production
&lt;/h2&gt;

&lt;p&gt;The ORM hides the queries. You write post.author and a SELECT fires behind your back. In development you have 10 rows and the page is instant. In production you have 10,000, one page fires thousands of queries, each query holds a database connection while it runs, the pool empties, every request waits, and the page you tested a hundred times goes down.&lt;/p&gt;

&lt;p&gt;A lot of "it worked yesterday" outages are exactly this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catch it in 60 seconds
&lt;/h2&gt;

&lt;p&gt;Turn on query logging in development, load ONE page, read the log:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hibernate: spring.jpa.show-sql=true&lt;/li&gt;
&lt;li&gt;Rails: config.log_level = :debug&lt;/li&gt;
&lt;li&gt;Django: enable logging for django.db.backends&lt;/li&gt;
&lt;li&gt;Prisma: log ["query"]&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same SELECT repeating with different ids means that page has an N+1. If the query count grows when your data grows, same disease.&lt;/p&gt;

&lt;h2&gt;
  
  
  When it's fine
&lt;/h2&gt;

&lt;p&gt;Not every N+1 is worth fixing. Three items on a dashboard? Leave it. Don't over-engineer. Fix it when N grows with your data. The senior move isn't fixing every one, it's knowing which ones matter.&lt;/p&gt;

&lt;p&gt;Fast pages aren't about fast queries. They're about fewer of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch it happen
&lt;/h2&gt;

&lt;p&gt;The full episode shows the counter climbing to 2,101 and collapsing to 3:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/BEuwQhLdgNs"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Numbers are from the demo in the video (generic posts/comments/authors schema), with the round trip simulated to model a remote database. Same-datacenter latency is sub-millisecond; cross-network can be tens of ms. The lesson is count times YOUR latency.&lt;/p&gt;

&lt;p&gt;What's the worst N+1 you've found in production?&lt;/p&gt;

</description>
      <category>database</category>
      <category>sql</category>
      <category>performance</category>
      <category>backend</category>
    </item>
    <item>
      <title>Why Your Database Index Gets Ignored (and How to Design One That Isn't)</title>
      <dc:creator>Dilip V P</dc:creator>
      <pubDate>Sat, 04 Jul 2026 10:33:38 +0000</pubDate>
      <link>https://dev.to/dilip_v_p/why-your-database-index-gets-ignored-and-how-to-design-one-that-isnt-apl</link>
      <guid>https://dev.to/dilip_v_p/why-your-database-index-gets-ignored-and-how-to-design-one-that-isnt-apl</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; An index can exist and still do nothing for your query. A multi-column index only serves queries that use its columns from the left, in the index's order. Fix it by putting the column you filter on first. Go further by putting every column the query needs inside the index (a covering index) so the database never touches the table. But every index taxes every write, so design them, don't collect them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Last time I showed what happens with no index: the database reads every row. This is the sneakier version.&lt;/p&gt;

&lt;p&gt;You added the index. &lt;code&gt;EXPLAIN&lt;/code&gt; still says the table is being scanned. The index isn't broken, and the database isn't being dumb. The index just cannot serve that 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;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_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;last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;EXPLAIN&lt;/span&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;users&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Martha'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- SCAN users        &amp;lt;- ignored&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The left-prefix rule
&lt;/h2&gt;

&lt;p&gt;A multi-column index keeps rows sorted by its first column, then the next: exactly like a phone book, last name then first name.&lt;/p&gt;

&lt;p&gt;Search by last name and you jump straight to the page. Search by first name alone and the sorting cannot help you: the Marthas are scattered across every page. There is no way to jump, so the database reads the whole thing.&lt;/p&gt;

&lt;p&gt;That is the left-prefix rule: &lt;strong&gt;an index serves a query only when the filter starts from the index's first column.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;WHERE last_name = ?&lt;/code&gt; -&amp;gt; uses the index&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WHERE last_name = ? AND first_name = ?&lt;/code&gt; -&amp;gt; uses the index&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WHERE first_name = ?&lt;/code&gt; -&amp;gt; full scan
One thing people get wrong: the order of conditions in your SQL means nothing. &lt;code&gt;WHERE a = ? AND b = ?&lt;/code&gt; and &lt;code&gt;WHERE b = ? AND a = ?&lt;/code&gt; produce the identical plan. Only the column order inside the index counts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The fix, measured
&lt;/h2&gt;

&lt;p&gt;Rebuild the index so the column you filter on comes first:&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_first_last&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;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my test table (SQLite, in-memory, 1M rows), that same query flipped from &lt;code&gt;SCAN users&lt;/code&gt; at 26.4 ms to &lt;code&gt;SEARCH users USING INDEX&lt;/code&gt; at 0.02 ms. Roughly 1,300x. Your absolute numbers will differ; the plan flip is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Covering indexes: never touch the table
&lt;/h2&gt;

&lt;p&gt;A normal index holds just two things: the columns it is sorted on, and a pointer to the row. So after it finds your match, it takes a second hop to the table to grab the other columns you asked for.&lt;/p&gt;

&lt;p&gt;But if every column your query needs is already in the index, that second hop disappears. The database answers from the index alone:&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="c1"&gt;-- index: (first_name, last_name, city)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&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;first_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Martha'&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 names it differently per database:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Covering hit looks like&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;&lt;code&gt;USING COVERING INDEX&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Index Only Scan&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Extra: Using index&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In MySQL/InnoDB the saving is doubled: secondary indexes store the primary key as the row pointer, so the "second hop" is itself another index lookup. (Postgres uses heap tables, so the mechanics differ; &lt;code&gt;Index Only Scan&lt;/code&gt; is the thing to look for.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What every index costs your writes
&lt;/h2&gt;

&lt;p&gt;An index is a B-tree kept in perfect sorted order. That order is what makes reads fast, and it is exactly what writes have to pay for.&lt;/p&gt;

&lt;p&gt;Every insert walks down the tree and slots the new value into its exact place. When a node fills up, it splits in two and promotes a key to its parent. That split can ripple upward and grow the whole tree a level. One small insert can rewrite several nodes, and every index on the table is another tree the database keeps balanced on every single write.&lt;/p&gt;

&lt;h2&gt;
  
  
  When NOT to add an index
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Low-selectivity columns.&lt;/strong&gt; An order status or a yes/no flag matches half the table. Jumping saves nothing, so the planner scans anyway. The index just sits there taxing writes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write-heavy, read-light tables.&lt;/strong&gt; If you write far more often than you read, the tax outweighs the benefit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redundant prefixes.&lt;/strong&gt; An index on &lt;code&gt;(last_name, first_name)&lt;/code&gt; already serves &lt;code&gt;last_name&lt;/code&gt; queries. Adding another index on &lt;code&gt;last_name&lt;/code&gt; alone is write cost for nothing.
Index the columns you actually filter, join, and sort on: the ones that narrow to a few rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The principle
&lt;/h2&gt;

&lt;p&gt;An index isn't a box you tick. It is a structure you design: the right column order so it gets used, the right columns inside so it covers, and only where reads outweigh writes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch it visually
&lt;/h2&gt;

&lt;p&gt;I made a short, visual breakdown of all of this: the phone book, the real EXPLAIN flip, the covering hit, and the B-tree split animation:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/JbFWtfFA4xE"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;It is episode two of &lt;strong&gt;Inside the Database&lt;/strong&gt;, part of The Leap, a series explaining the systems we build on from first principles. Next up: how loading one page can quietly fire a hundred queries, and nobody notices.&lt;/p&gt;

&lt;p&gt;What is the most confusing plan &lt;code&gt;EXPLAIN&lt;/code&gt; has ever shown you?&lt;/p&gt;

</description>
      <category>database</category>
      <category>sql</category>
      <category>backend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How Database Indexes Actually Work (and When They Backfire)</title>
      <dc:creator>Dilip V P</dc:creator>
      <pubDate>Mon, 29 Jun 2026 06:58:15 +0000</pubDate>
      <link>https://dev.to/dilip_v_p/how-database-indexes-actually-work-and-when-they-backfire-2c59</link>
      <guid>https://dev.to/dilip_v_p/how-database-indexes-actually-work-and-when-they-backfire-2c59</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Without an index, your database finds a row by reading &lt;em&gt;every&lt;/em&gt; row (a full table scan). An index is a sorted structure that lets it jump straight to the row instead. But indexes are a trade, not free speed: they only help selective queries, and they slow down every write. Use &lt;code&gt;EXPLAIN&lt;/code&gt; to see what your database is actually doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Your query was instant in development. In production, it crawls. Same code. The only thing that changed is the amount of data.&lt;/p&gt;

&lt;p&gt;Nine times out of ten, this is why: without an index, the database has only one way to find your row, which is to read every row, one at a time, until it matches. That is a full table scan.&lt;/p&gt;

&lt;p&gt;On 10,000 rows you don't notice. On 10,000,000, it is painful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an index exists (first principles)
&lt;/h2&gt;

&lt;p&gt;An index exists to avoid that work. Scanning the whole table doesn't scale: a query that filters on one column shouldn't have to read every row to find a handful.&lt;/p&gt;

&lt;p&gt;So the database keeps a separate, sorted directory of one column's values, stored alongside the table, like the index at the back of a textbook. Instead of flipping through every page, you look up the term and jump straight to the page.&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;h2&gt;
  
  
  Why it's fast
&lt;/h2&gt;

&lt;p&gt;Because the directory is sorted, the database doesn't read it top to bottom either. It navigates straight toward the value: narrow the range, discard the half that can't contain it, repeat. That is the same halving idea as binary search, and the structure that makes it work on disk is a B-tree (the disk-friendly generalization, not literally a binary search over rows).&lt;/p&gt;

&lt;p&gt;The payoff: finding one row among a million takes on the order of ~20 steps, not a million.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part most engineers miss: it's a trade
&lt;/h2&gt;

&lt;p&gt;Indexes are not free performance. They are a trade-off:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;They only help when your query is selective&lt;/strong&gt;, when it returns a small fraction of the table. &lt;code&gt;WHERE id = ?&lt;/code&gt; (one row) flies. &lt;code&gt;WHERE active = true&lt;/code&gt; (half the table) can be &lt;em&gt;slower&lt;/em&gt; with the index than a plain scan, because there is no shortcut when you are returning most of the rows. A plan can literally say "using index" and still be slow; what matters is how many rows it touches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every index has a write cost.&lt;/strong&gt; Inserts, updates, and deletes all have to keep every index current. The more indexes you have, the slower your writes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;They cost storage.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the rule isn't "add indexes everywhere." It is: index the columns your queries actually filter, join, and sort on, and only where it is selective enough to help.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop guessing: read the plan
&lt;/h2&gt;

&lt;p&gt;You never have to guess at any of this. Put &lt;code&gt;EXPLAIN&lt;/code&gt; in front of your query and the database shows you its plan before it runs anything.&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="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;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="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you are looking for is "reads the whole table" turning into "uses the index":&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Slow (full scan)&lt;/th&gt;
&lt;th&gt;Fast (uses index)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SCAN users&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SEARCH users USING INDEX&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Seq Scan&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Index Scan&lt;/code&gt; / &lt;code&gt;Index Only Scan&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;type: ALL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;type: ref&lt;/code&gt; / &lt;code&gt;range&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(In Postgres, &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; also shows the real row counts versus the planner's estimates, which is where a lot of "why didn't it use my index?" mysteries get solved.)&lt;/p&gt;

&lt;p&gt;Get in the habit of reading the plan, and you will catch the slow query before it pegs your database and takes the app down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch it visually
&lt;/h2&gt;

&lt;p&gt;I made a short, visual breakdown of all of this, from the full table scan to the index, to why it is fast, to when it backfires, to reading &lt;code&gt;EXPLAIN&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/ai_Kx7vjbPA"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;It is the first episode of &lt;strong&gt;The Leap&lt;/strong&gt;, a series explaining the systems we build on, from databases and networking to memory and distributed systems, from first principles. Next up: the dark side of indexes, and when adding one is the wrong move.&lt;/p&gt;

&lt;p&gt;What's the nastiest slow query you have had to debug in production?&lt;/p&gt;

</description>
      <category>database</category>
      <category>sql</category>
      <category>backend</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
