<?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: Adam Jones</title>
    <description>The latest articles on DEV Community by Adam Jones (@awj).</description>
    <link>https://dev.to/awj</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F89750%2Fe2d3cd79-87d4-4b92-b397-08c55cbf61e6.jpeg</url>
      <title>DEV Community: Adam Jones</title>
      <link>https://dev.to/awj</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/awj"/>
    <language>en</language>
    <item>
      <title>Understanding database indices by (poorly) implementing one</title>
      <dc:creator>Adam Jones</dc:creator>
      <pubDate>Sat, 20 May 2023 20:59:12 +0000</pubDate>
      <link>https://dev.to/awj/understanding-database-indices-by-poorly-implementing-one-436f</link>
      <guid>https://dev.to/awj/understanding-database-indices-by-poorly-implementing-one-436f</guid>
      <description>&lt;p&gt;There’s a lot of misconceptions about database indices. These exist, in part, because people are missing the context needed to imagine how a database uses them. There’s &lt;em&gt;a lot&lt;/em&gt; to learn to establish that context. Too much for one blog post. But, we can try to bootstrap off what’s already familiar to help develop a better understanding.&lt;/p&gt;

&lt;p&gt;To do that, we’re going to implement a fake database index in Ruby. It will be &lt;em&gt;woefully&lt;/em&gt; incomplete, but still should be enough to give an idea of what’s happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warning
&lt;/h2&gt;

&lt;p&gt;What you’ll see here is not, &lt;em&gt;actually&lt;/em&gt;, how database indices work. It’s an extremely crude approximation. I try to call out the where and how that approximation isn’t valid. If you encounter anything in an actual database that doesn’t match up with what you see here, I encourage you to take that as an opportunity to dive in and learn deeper.&lt;/p&gt;

&lt;h1&gt;
  
  
  Making things &lt;em&gt;very&lt;/em&gt; simple
&lt;/h1&gt;

&lt;p&gt;We’re going to build our fake index out the humble Ruby &lt;code&gt;Hash&lt;/code&gt;. Those are pretty familiar, right? Store data by key and value, then you can later retrieve the value by providing the key. If you don’t have a key, you’re basically just working with a more expensive variant of an &lt;code&gt;Array&lt;/code&gt;. Ironically, under the hood a Ruby &lt;code&gt;Hash&lt;/code&gt; uses a lot of the same concepts and data structures as database indices. Anyways, this will be our substitute for writing actual data structure code.&lt;/p&gt;

&lt;p&gt;We’ll only support &lt;em&gt;unique&lt;/em&gt; indices. It’s possible, but messy, for us to support non-unique ones. I just don’t think it’s going to teach much you won’t already learn here. We &lt;em&gt;will&lt;/em&gt; support composite indices, and will get into covering queries that only use some of the index columns.&lt;/p&gt;

&lt;p&gt;Probably the biggest query-time thing separating our index from a real one will be lack of support for range queries. So no &lt;code&gt;WHERE X &amp;gt; 0&lt;/code&gt; style queries for our index. We’re ignoring this because hashes don’t make it easy to do efficiently, and I don’t think implementing it will tell you much that direct value lookups don’t. Real database indices &lt;em&gt;absolutely&lt;/em&gt; are able to handle these for many different data types.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Index class
&lt;/h1&gt;

&lt;p&gt;We’ll start with a class, that we name &lt;code&gt;Index&lt;/code&gt;, which will be the core of our code here. We will “implement” different SQL queries as Ruby code written in terms of this &lt;code&gt;Index&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;We use &lt;code&gt;Index.declare&lt;/code&gt; to create an (empty) index on a list of columns. Then we can add data to it by looping through the data and calling &lt;code&gt;Index#add&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Allow us to efficiently answer questions about a large amount of data based on&lt;/span&gt;
&lt;span class="c1"&gt;# specific column(s) in it.&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Index&lt;/span&gt;
  &lt;span class="c1"&gt;# The column this index is handling.&lt;/span&gt;
  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:column&lt;/span&gt;

  &lt;span class="c1"&gt;# The columns that come *after* this one in the index.&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="c1"&gt;# If this list is empty, we're at the "end" of the index column list and&lt;/span&gt;
  &lt;span class="c1"&gt;# should store row ids as our Hash values.&lt;/span&gt;
  &lt;span class="c1"&gt;#&lt;/span&gt;
  &lt;span class="c1"&gt;# If it is *not* empty, we make an `Index` class that deals with those&lt;/span&gt;
  &lt;span class="c1"&gt;# columns and use it as our Hash value.&lt;/span&gt;
  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:subsequent_columns&lt;/span&gt;

  &lt;span class="c1"&gt;# The Hash that represents actual index content. I'm avoiding calling this&lt;/span&gt;
  &lt;span class="c1"&gt;# `data` because it's *not* the actual data we're indexing. Confusing&lt;/span&gt;
  &lt;span class="c1"&gt;# terminology.&lt;/span&gt;
  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:content&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subsequent_columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="vi"&gt;@column&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;
    &lt;span class="vi"&gt;@subsequent_columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subsequent_columns&lt;/span&gt;
    &lt;span class="vi"&gt;@content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# Are we the final column of the index? If so, our answers should be data id&lt;/span&gt;
  &lt;span class="c1"&gt;# values instead of another `Index`&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;leaf?&lt;/span&gt;
    &lt;span class="vi"&gt;@subsequent_columns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# "Index" a piece of data. It's assumed that this data is functionally a Hash&lt;/span&gt;
  &lt;span class="c1"&gt;# that contains at least `:id` and whatever value we hvae for `column`.&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;leaf?&lt;/span&gt;
      &lt;span class="vi"&gt;@content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="c1"&gt;# If we are *not* the final column, create a new Index to represent the&lt;/span&gt;
      &lt;span class="c1"&gt;# slice of data that all shares the same value for our `column`. This&lt;/span&gt;
      &lt;span class="c1"&gt;# index should use the *next* subsequent column, and needs to know about&lt;/span&gt;
      &lt;span class="c1"&gt;# the *rest* of the subsequent columns in case it too is not the final&lt;/span&gt;
      &lt;span class="c1"&gt;# one.&lt;/span&gt;
      &lt;span class="vi"&gt;@content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="no"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subsequent_columns&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;subsequent_columns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop&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="vi"&gt;@content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What we can learn about database indices
&lt;/h2&gt;

&lt;p&gt;Surprisingly, just here we can draw an important and useful inference about working with indices. The “natural flow” of accessing this data is going to be along the path dictated by the columns. Our index also can’t answer questions involving columns that weren’t indexed.&lt;/p&gt;

&lt;p&gt;It’s easy to imagine navigating this in column order, but &lt;em&gt;other&lt;/em&gt; orders seem like a bigger challenge. Databases are full of clever optimizations that can &lt;em&gt;sometimes&lt;/em&gt; make out-of-order usage possible, but generally speaking you want things to happen in-order.&lt;/p&gt;

&lt;h1&gt;
  
  
  Sample data
&lt;/h1&gt;

&lt;p&gt;To play with this, we’ll work on sample data taken from the &lt;a href="https://www.census.gov/data/tables/time-series/demo/popest/2020s-total-cities-and-towns.html#v2022"&gt;US Census Bureau City and Town Population Totals&lt;/a&gt; This is a list of ~20k cities in the US with their estimated population.&lt;/p&gt;

&lt;p&gt;For the purposes of this post, I have &lt;a href="https://awj.dev/static/city_populations_2022.csv"&gt;Cleaned it up&lt;/a&gt; in a CSV, with state names extracted.&lt;/p&gt;

&lt;p&gt;We’re going to assume here that the combination of the &lt;code&gt;city&lt;/code&gt; and &lt;code&gt;state&lt;/code&gt; columns makes a record unique. That isn’t &lt;em&gt;strictly&lt;/em&gt; true for this data, but again it makes it easier to work with.&lt;/p&gt;

&lt;h1&gt;
  
  
  Harnass code
&lt;/h1&gt;

&lt;p&gt;The following code is enough to get us started in an IRB session. It assumes the above code snippet is available locally as &lt;code&gt;./index.rb&lt;/code&gt;, and the csv can be found at &lt;code&gt;./city_populations_2022.csv&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"csv"&lt;/span&gt;

&lt;span class="nb"&gt;load&lt;/span&gt; &lt;span class="s2"&gt;"index.rb"&lt;/span&gt;

&lt;span class="c1"&gt;# Load the CSV, converting integer values as we go&lt;/span&gt;
&lt;span class="n"&gt;csv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CSV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"./city_populations_2022.csv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;headers: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;converters: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:integer&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Store our CSV in an Array where the values are hashes of the row&lt;/span&gt;
&lt;span class="c1"&gt;# data. This will simulate the actual database table.&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:to_h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;

&lt;span class="c1"&gt;# Declare an index on state and city, in that order&lt;/span&gt;
&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;row&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;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we were to discard the index class and &lt;em&gt;just&lt;/em&gt; look at things as nested hashes, our index would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"California"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"Los Angeles"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1444&lt;/span&gt; &lt;span class="c1"&gt;# 1444 is the row id for this city&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;h1&gt;
  
  
  Finding a row by state and city
&lt;/h1&gt;

&lt;p&gt;We’ll start out simple: given a city and state, look up the row. We’ll try it out with Los Angeles, California. In SQL, this would be: &lt;code&gt;SELECT * FROM populations WHERE state = 'California' AND city = 'Los Angeles'&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;state&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;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"California"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Los Angeles"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Our `id` values don't exactly correspond to Array offsets, so we have to do this.&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;city&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;h2&gt;
  
  
  What we can learn about database indices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Index ordering
&lt;/h3&gt;

&lt;p&gt;Notice how we’re starting the lookup with the &lt;code&gt;state&lt;/code&gt;? That’s because it’s the “beginning” of the index.&lt;/p&gt;

&lt;p&gt;Imagine if we tried to start with the &lt;code&gt;city&lt;/code&gt; first. What would that code look like? It would have to dig through &lt;em&gt;every value&lt;/em&gt; in the &lt;code&gt;state&lt;/code&gt; index to get at cities, then work its way backwards.&lt;/p&gt;

&lt;p&gt;Often, your database effectively can’t do this. There’s too much data involved, and simply keeping track of everything you’ve looked at could cause problems. Plus “examine the entire index” isn’t going to be a fast operation. It might pursue this strategy if you give it no better option, but you &lt;em&gt;really&lt;/em&gt; want to give it better options.&lt;/p&gt;

&lt;h3&gt;
  
  
  Row lookup
&lt;/h3&gt;

&lt;p&gt;Notice how, to return the data, we had to go to our “table” that is stored in &lt;code&gt;data&lt;/code&gt;? That’s called a “row lookup”. Real databases almost certainly store the index data and row data separately, so row lookups have additional overhead that we want to be careful with.&lt;/p&gt;

&lt;p&gt;Often, optimizing SQL queries is a process of trying to avoid any more row lookups than strictly necessary.&lt;/p&gt;

&lt;h1&gt;
  
  
  Finding the total population of a state
&lt;/h1&gt;

&lt;p&gt;Ok, now let’s try another likely task: finding the total population of a state. We’ll go with Idaho this time. In SQL this would look like &lt;code&gt;SELECT sum(population) FROM populations WHERE state = 'Idaho'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At first glance it might not look like our index is helpful here, but it still is. Here’s code to get this &lt;em&gt;without&lt;/em&gt; the index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="c1"&gt;# Let's keep track of how many times we had to go fetch a row. This is&lt;/span&gt;
&lt;span class="c1"&gt;# important, because row lookups are expensive.&lt;/span&gt;
&lt;span class="n"&gt;rows_examined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="c1"&gt;# Notice: we are visiting *every* row in the data. If we had millions or&lt;/span&gt;
&lt;span class="c1"&gt;# billions of rows, this would be really bad.&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;rows_examined&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;next&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"Idaho"&lt;/span&gt;

  &lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"population"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows_examined&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; [1302154, 19692]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So we got our sum, it &lt;em&gt;probably&lt;/em&gt; was fast on your computer (reminder: this is a tiny amount of data), but we had to look at every single row in the data. Usually, “we have to look at every row in the entire table” is one of the absolute &lt;em&gt;worst&lt;/em&gt; things you can see your database doing.&lt;/p&gt;

&lt;p&gt;So how can we use our index? We don’t have a ready list of “the names of every city in Idaho”, so we can’t just plug that in as keys once we get to the &lt;code&gt;Idaho&lt;/code&gt; index. But, we &lt;em&gt;do&lt;/em&gt; have the ability to traverse a &lt;code&gt;Hash&lt;/code&gt; by &lt;em&gt;values&lt;/em&gt;. So we can still use our index to help us get to the state of Idaho, then crawl through its contents to find the total population.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="c1"&gt;# Again, we're tracking rows&lt;/span&gt;
&lt;span class="n"&gt;rows_examined&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="n"&gt;state&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;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Idaho'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;row_id&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row_id&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;rows_examined&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"population"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rows_examined&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; [1302154, 199]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So now we have the &lt;em&gt;same&lt;/em&gt; sum, but we looked at roughly 10% of the rows. That’s a &lt;em&gt;huge&lt;/em&gt; win.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we can learn about database indices
&lt;/h2&gt;

&lt;p&gt;Databases don’t just use indices for cases where they have every single relevant key. It’s a data structure that they can dig through, and that can help significantly.&lt;/p&gt;

&lt;p&gt;Sometimes they do this by “skipping over” intermediate keys to get to the final rows, like what we did here. It’s worth noticing that this was only possible because our index was defined as &lt;code&gt;(state, city)&lt;/code&gt;. If it had been &lt;code&gt;(city, state)&lt;/code&gt;, we would have had to examine every single city name to see if it was in the state. That’s usually still &lt;em&gt;better&lt;/em&gt; than crawling every row of the data, but it’s nowhere near as good as what we just experienced.&lt;/p&gt;

&lt;p&gt;When you’re defining a composite index, it’s &lt;em&gt;really&lt;/em&gt; important to think about the cases where you might end up querying only some of those columns. Getting the column order right will maximize the value you get out of the database’s work in maintaining the index.&lt;/p&gt;

&lt;h1&gt;
  
  
  A new index for even faster population totals
&lt;/h1&gt;

&lt;p&gt;Let’s say this kind of population query is extremely important, and we’ve found the above “only accessing 10% of the rows” to &lt;em&gt;still&lt;/em&gt; be too slow for our needs. What can an index do for us?&lt;/p&gt;

&lt;p&gt;We’ve done more or less everything we can with our existing index. If our system supported non-unique indices, we could make an index on just &lt;code&gt;state&lt;/code&gt; that would allow us to directly jump into rows, but it wouldn’t change the amount of rows we’re looking at.&lt;/p&gt;

&lt;p&gt;Let’s build &lt;em&gt;another&lt;/em&gt; index, one that extends our previous index with population values. So it would look like &lt;code&gt;(state, city, population)&lt;/code&gt;. Here’s how:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;population_index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"population"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;population_index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;state+city&lt;/code&gt; was already unique, &lt;code&gt;state+city+population&lt;/code&gt; is also going to be.&lt;/p&gt;

&lt;p&gt;Here’s a sketch of it as a Hash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"California"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"Los Angeles"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;# NOTE: This "population" Hash will always be a single key (the&lt;/span&gt;
      &lt;span class="c1"&gt;# population) pointing to the row id.&lt;/span&gt;
      &lt;span class="mi"&gt;3898767&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1444&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;This index can give us our population total &lt;em&gt;without touching a single row&lt;/em&gt;!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;population_index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Idaho"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;

&lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; 1302154&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how &lt;code&gt;data&lt;/code&gt; is not even mentioned in this code. We’re answering queries &lt;em&gt;just&lt;/em&gt; from the index content!&lt;/p&gt;

&lt;h2&gt;
  
  
  What we can learn about database indices
&lt;/h2&gt;

&lt;p&gt;Since our index reflects the underlying data, we can use the index contents &lt;em&gt;in place of&lt;/em&gt; the actual data. Databases use this trick &lt;em&gt;a lot&lt;/em&gt;, and it’s an incredibly effective optimization.&lt;/p&gt;

&lt;p&gt;It’s generally safe to assume that your data on disk isn’t organized in a way that makes any particular lookup effective. Before when we read 199 rows to get our data, it’s safe to assume that none of those rows lived next to each other in a way that allowed the operating system to avoid doing 199 disk reads.&lt;/p&gt;

&lt;p&gt;By comparison, even when the index is serialized to disk, all of the relevant bits of information live closer together. It’s very likely that reading the disk block that gave us one relevant &lt;code&gt;city&lt;/code&gt; &lt;em&gt;also&lt;/em&gt; happened to load and cache other cities we needed. Plus our index data is a lot smaller/denser than the actual row data. So even digging everything up off the disk involved fewer disk reads.&lt;/p&gt;

&lt;p&gt;When trying to look up actual city records, the same “skip over a column” trick that we did in the last section can work here. So it’s possible to go from &lt;code&gt;(state, city, population)&lt;/code&gt; to the city record even with just &lt;code&gt;state&lt;/code&gt; and &lt;code&gt;city&lt;/code&gt;. This index could handily serve every query we’ve seen so far.&lt;/p&gt;

&lt;h1&gt;
  
  
  Finding the total population of EVERY state
&lt;/h1&gt;

&lt;p&gt;Now we’re going to try to handle this query: &lt;code&gt;SELECT state, sum(population) FROM populations GROUP BY state&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;STOP! Before you read further, I want you to think about how you’d solve this. You have three options now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Walk through all the data rows&lt;/li&gt;
&lt;li&gt;Try to use the original index&lt;/li&gt;
&lt;li&gt;Try to use the population index&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That act of “deciding how to get at the data” is called &lt;em&gt;Query Planning&lt;/em&gt;. It’s an important part of how databases work. Get deep enough into database performance and you’re going to have to become intimately familiar with your database’s query plan explanations. Examining that output is a key way to help debug slow queries and figure out what changes need to happen to make them not-slow queries.&lt;/p&gt;

&lt;p&gt;In this case we have only three options, and it’s (probably) relatively easy to pick which one will be “the best”. But, let’s think them through in a rough approximation of how a query planner might look at this.&lt;/p&gt;

&lt;p&gt;If we assign a “cost” to data and index reads, we can weigh our options by “total cost”:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Walk all the rows: 20,000 data reads + 0 index reads&lt;/li&gt;
&lt;li&gt;Use the original index: 20,000 data reads + 20,000 index reads&lt;/li&gt;
&lt;li&gt;Use the population index: 0 data reads + 20,000 index reads (reminder: all needed data is in the index)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s generally accurate to assume that index reads are cheaper than data reads. So we’d want to “weigh” data reads higher. The actual process inside a real database is much more complicated, but here we’ll just assume data reads are 5x as expensive.&lt;/p&gt;

&lt;p&gt;That gives us total costs of: 100,000 120,000 and 20,000 respectively. Which means we should go with the last option.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sidenote: to make it accessible, this cost calculation is wildly naive. Real databases track a lot more information than “how many rows are there”, and have more detailed insights about both the characteristics of the data and the specifics of how it is stored. Imagine you spent years refining this concept to fix every case where your cost predictions were wrong, and you’re closer to how databases actually work.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So, how do we find per-state populations? That one again comes out kind of straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="n"&gt;population_index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cities&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="n"&gt;cities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;population&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;population&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&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="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What we can learn about database indices
&lt;/h2&gt;

&lt;p&gt;Again, we’re using the index as a &lt;em&gt;source&lt;/em&gt; of information rather than just a way to &lt;em&gt;get to&lt;/em&gt; information. It’s worth reiterating this because it comes up so often in real world scenarios.&lt;/p&gt;

&lt;p&gt;We also, in our simulated query planning, saw a case where using an index was &lt;em&gt;slower&lt;/em&gt; than reading the entire table. In practice, these scenarios are rare, but they can happen. Sometimes you’ll look at a query plan and wonder why the database is ignoring an index, only to find that you’re missing details where the index makes things &lt;em&gt;slower&lt;/em&gt;. In this case that detail was “we’re asking to read the entire table”, but it’s definitely not the only one.&lt;/p&gt;

&lt;p&gt;We’re also, in our &lt;code&gt;result&lt;/code&gt; hash, just getting a glimpse into result buffering. Again it’s worth imagining what we would do in a scenario where there was so much data that just storing this &lt;code&gt;result&lt;/code&gt; in memory wouldn’t work.&lt;/p&gt;

&lt;h1&gt;
  
  
  Wrapping up
&lt;/h1&gt;

&lt;p&gt;Hopefully this helps a little to demistify database indices. As I mentioned at the start, the &lt;em&gt;actual&lt;/em&gt; data structures vary significantly from what we’re using here, but I’ve tried to keep the reasoning and thought processes consistent.&lt;/p&gt;

&lt;p&gt;Despite the inaccuracies, you can get pretty far using this “wandering through nested hashes” view of how database indices work. Perhaps the best extension to your mental model would be imagining a hash that also includes the ability to do inequality comparisons on keys. Like if a &lt;code&gt;Hash#lookup&lt;/code&gt; method existed that took a Ruby &lt;code&gt;Range&lt;/code&gt; as an argument and could efficiently give you the values where keys were inside of that range.&lt;/p&gt;

&lt;p&gt;If all of this has you interested in what the internals of an index &lt;em&gt;actually&lt;/em&gt; look like, you can start by studying &lt;a href="https://en.wikipedia.org/wiki/B-tree#:~:text=In%20computer%20science%2C%20a%20B,with%20more%20than%20two%20children."&gt;B-trees&lt;/a&gt;. They’re probably the most commonly used data structure for this purpose. Many databases support alternative index types based around different data structures, which is where you really start getting deep into the benefits and drawbacks of each one.&lt;/p&gt;

&lt;p&gt;If you’d like to know more about query plans and how databases handle the topic of picking an algorithm to look up the data, that unfortunately gets pretty specific to the database involved. If you’re using &lt;a href="https://dev.mysql.com/doc/refman/8.0/en/execution-plan-information.html"&gt;MySQL&lt;/a&gt; or &lt;a href="https://www.postgresql.org/docs/current/using-explain.html"&gt;PostgreSQL&lt;/a&gt; I’ve linked to the relevant sections of their documentation. Because databases are attempting to generate the best possible plan out of (potentially) a huge number of choices in a tiny fraction of time, query planning gets kind of hairy and detailed fast.&lt;/p&gt;

&lt;p&gt;If this has peaked your interest in how to effectively use indices, &lt;a href="https://use-the-index-luke.com/"&gt;Use the index, Luke&lt;/a&gt; is a fantastic resource. It even includes an introduction to B-trees and resources tailored to multiple database types.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>database</category>
      <category>performance</category>
    </item>
    <item>
      <title>Understanding MySQL Multiversion Concurrency Control</title>
      <dc:creator>Adam Jones</dc:creator>
      <pubDate>Sun, 16 Sep 2018 18:02:55 +0000</pubDate>
      <link>https://dev.to/awj/understanding-mysql-multiversion-concurrency-control-1n5f</link>
      <guid>https://dev.to/awj/understanding-mysql-multiversion-concurrency-control-1n5f</guid>
      <description>&lt;p&gt;MySQL, under the InnoDB storage engine, allows writes and reads of the same row to not interfere with each other. This is one of those features that we use so often it kind of gets taken for granted, but if you think about how you would build such a thing it’s a lot more detailed than it seems. Here, I am going to talk through how that is implemented, as well as some ramifications of the design.&lt;/p&gt;

&lt;h1&gt;
  
  
  Allowing Concurrent Change
&lt;/h1&gt;

&lt;p&gt;Unsurprisingly, given the title of this post, MySQL’s mechanism for allowing you to simultaneously read and write from the same row is called “Multiversion Concurrency Control”. They (of course) &lt;a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-multi-versioning.html"&gt;have documentation&lt;/a&gt; on it, but that dives into internal technical details pretty fast.&lt;/p&gt;

&lt;p&gt;Instead, let’s talk about it at a little higher level. This concept has been around for a long time (the best I can do hunting down an origin is a &lt;a href="https://dspace.mit.edu/handle/1721.1/16279"&gt;thesis&lt;/a&gt; from 1979). The overall answer for allowing concurrent reads and writes is pretty simple: writes create new versions of rows, reads see the version that was current when they started.&lt;/p&gt;

&lt;h1&gt;
  
  
  Version tracking
&lt;/h1&gt;

&lt;p&gt;Obviously if we’re going to keep track of versions, we need something to differentiate them. This tool needs to distinguish one version from another, but ideally it would also make it easy to decide which version a read operation should see.&lt;/p&gt;

&lt;p&gt;In MySQL, this “version enabling thing” is a transaction id. Every transaction gets one. Even your one-shot update queries in the console get one. These ids are incremented in a way that allows MySQL to determine that one transaction started before another. Every table under InnoDB essentially has a “hidden column” that stores the transaction id of the last write operation to change the row. So, in addition to the columns you may have updated, a write operation &lt;em&gt;also&lt;/em&gt; marks the row with its transaction id. This allows read operations to know if they can use the row data, or if it has been changed and they need to consult an older version.&lt;/p&gt;

&lt;h1&gt;
  
  
  Reading older version
&lt;/h1&gt;

&lt;p&gt;For the cases where your read operation hits on rows that have been changed, you’ll need an older version of the data. The transaction id comes into play here too, but there’s more info needed. Every time MySQL writes data into a row, it &lt;em&gt;also&lt;/em&gt; writes an entry into the rollback segment. This is a data structure that stores “undo logs” used to restore the row to its previous state. It’s called the “rollback segment” because it is the tool used to handle rolling back transactions.&lt;/p&gt;

&lt;p&gt;The rollback segment stores undo logs for each row in the database. Every row has &lt;em&gt;another&lt;/em&gt; hidden column that stores the location of the latest undo log entry, which would restore the row to its state prior to the last write. When these entries are created, they are marked with the &lt;em&gt;outgoing&lt;/em&gt; transaction id. By walking the undo log for a row and finding the latest transaction &lt;em&gt;before&lt;/em&gt; a read transaction the database can identify the correct data to present to a transaction.&lt;/p&gt;

&lt;h1&gt;
  
  
  Handling deletes
&lt;/h1&gt;

&lt;p&gt;Deletion is handled by a marker in the row to indicate a record was deleted. Delete operations &lt;em&gt;also&lt;/em&gt; set the row’s transaction id to their transaction id, so the process above can present a pre-delete version of the row to read operations that started before the delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  When are versions deleted
&lt;/h2&gt;

&lt;p&gt;MySQL obviously cannot keep a record of every change that happens in the database for all time. It doesn’t need to, though. Undo logs can be removed as soon as the last transaction that could possibly want them completes.&lt;/p&gt;

&lt;p&gt;Similarly, rows that have been marked as deleted can be outright abandoned once the oldest active transaction started after the deletion. These rows and undo logs are physically removed to reclaim their disk space by a “purge” operation that happens in its own thread in the background.&lt;/p&gt;

&lt;h1&gt;
  
  
  What about indexes
&lt;/h1&gt;

&lt;p&gt;So, to recap, MySQL handles versions by keeping the row constantly up to date and storing diffs for as long as currently running queries need them. That’s only half the story though, indexes need to support consistent reads as well. Primary key indexes work much like the above description for actual database rows. Secondary indexes are a little different.&lt;/p&gt;

&lt;p&gt;MySQL handles this in two ways: pages of index entries are marked by the last transaction id to write in them, and individual index entries have delete markers. When an update changes an indexed column, three things happen: the current index entry is delete marked, a new entry for the updated value is written, and that new entry’s index page is marked with the transaction id.&lt;/p&gt;

&lt;p&gt;Read operations that find non-delete-marked entries in pages that predate their transaction id use that index data directly. If the operation finds either a delete marker or a newer transaction id, it looks up the row and traverses the undo log to find the appropriate value to use.&lt;/p&gt;

&lt;p&gt;Similar to the purging of deleted rows from expired transactions, delete-marked index entries are also eventually reclaimed. Because there is always a fresh new entry to work with &lt;em&gt;somewhere&lt;/em&gt; in the index, MySQL can be a little more aggressive at cleaning those up.&lt;/p&gt;

&lt;h1&gt;
  
  
  What do I do with this information?
&lt;/h1&gt;

&lt;p&gt;So, given the above, what can we take home to make our lives better? A few things. Keep in mind with all of this that database performance can be very difficult to analyze. Each point below is just one potential piece of the story of what could be happening with your data.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Big transactions are painful: Long running transactions don’t just tie up a connection, they force the database to preserve history for longer. If that transaction is reading through a large swath of the database subsequent writes will force it to read the rollback log, which may be in a different page of memory or even on disk.&lt;/li&gt;
&lt;li&gt;Multi-statement transactions need to commit quickly: This is another variety of “big transactions are painful”, but it’s worth calling out. MySQL does not “kill” active transactions. If you open a transaction, query out data, then spend two hours in application code before committing, MySQL will faithfully preserve undo history for two hours. Every moment of an open transaction forces more undo history. Commit as quickly as you can and do your processing outside of transactions whenever possible.&lt;/li&gt;
&lt;li&gt;Writes make index scans less useful. The whole point of an index is to answer questions about your data without actually looking at your data. Delete markers on index entries, and transaction stamps on index pages, force the database to read your data. Think carefully about using composite indexes with columns you aren’t querying. Your queries will pay the price for updates to those columns anyways.&lt;/li&gt;
&lt;li&gt;Rapid fire writes magnify the penalties for reads. If you have a lot of data to write, especially to the &lt;em&gt;same row&lt;/em&gt;, write it in chunks instead of one-by-one. Each write generates a transaction id, relevant undo logs, and makes a mess of secondary indexes. Chunking writes together increases the chances that reads will find valid index data, and lowers the size of undo logs they have to wade through. There’s an opposite extreme where one big write might have too much data, so it’s important to look for the happy middle ground here.&lt;/li&gt;
&lt;li&gt;“Hot” rows are hot for all columns, not just updated ones. A row that stores a frequently updated counter forces more row transaction id updates and undo log entries. Queries that start before the counter is incremented, even if they don’t use the counter, still have to traverse undo logs for the row state when they started. That same logic applies to extremely frequently updated timestamps that aggregate change times across the relations to a row. If possible, batch those updates beforehand or consider storing them in a separate table you can join to when needed.&lt;/li&gt;
&lt;li&gt;Consider separating reporting from direct/application use reads. Reporting queries tend to scan large sections of the database. They take a long time and thus force the preservation and consumption of more undo history. Most application behavior is more direct: it knows specific records to retrieve and goes straight for them. If you’re already using read replicas, consider dedicating one to reporting so that your application queries don’t pay the undo storage penalties of reporting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Final thoughts
&lt;/h1&gt;

&lt;p&gt;It’s worth noting that this is not the only way to implement MVCC. PostgreSQL handles this task by storing the minimum and maximum transaction ids where a row should be visible. Under that scheme, updating a row sets the maximum transaction id on it and creates an entirely new row entry by copying the original and performing the update in that copy. This avoids the need for undo logs, but at the cost of copying all row data for each update.&lt;/p&gt;

&lt;p&gt;The point here is that, for cases where you are really trying to push database performance, understanding a few of the bigger details of the internals can pay off. Many of the takeaways I listed are only going to be applicable in extreme use cases, but in those cases knowing how the database goes about versioning data can make understanding performance problems easier.&lt;/p&gt;

</description>
      <category>mysql</category>
    </item>
    <item>
      <title>Understanding the Elasticsearch Percolator</title>
      <dc:creator>Adam Jones</dc:creator>
      <pubDate>Tue, 24 Apr 2018 18:02:55 +0000</pubDate>
      <link>https://dev.to/awj/understanding-the-elasticsearch-percolator-2j5l</link>
      <guid>https://dev.to/awj/understanding-the-elasticsearch-percolator-2j5l</guid>
      <description>&lt;p&gt;Elasticsearch is a powerful, feature-packed tool. Their &lt;a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html"&gt;documentation&lt;/a&gt; is great, but some pieces are a bit … out there. Beyond that, some of the functionality has changed significantly over the years, so third-party explanations might no longer be accurate.&lt;/p&gt;

&lt;p&gt;One fantastic feature that is both unusual and has changed a lot is percolation. I’m going to try to explain that feature, in the context of its current implementation (version 6.2.4 as of this writing). You’ll need a basic understanding of Elasticsearch, specifically &lt;a href="https://www.elastic.co/guide/en/elasticsearch/reference/6.2/mapping.html"&gt;mappings&lt;/a&gt; and &lt;a href="https://www.elastic.co/guide/en/elasticsearch/reference/6.2/search-request-body.html"&gt;search&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Concept
&lt;/h1&gt;

&lt;p&gt;The normal workflow for Elasticsearch is to store documents (as JSON data) in an index, and execute searches (also JSON data) to ask the index about those documents.&lt;/p&gt;

&lt;p&gt;Succinctly, percolation reverses that. You store searches and use documents to ask the index about those searches. That’s true, but it’s not particularly actionable information. How percolators are structured has evolved over the years, to the point where we can give a more useful explanation.&lt;/p&gt;

&lt;p&gt;Now, percolation revolves around the &lt;a href="https://www.elastic.co/guide/en/elasticsearch/reference/6.2/percolator.html"&gt;percolator&lt;/a&gt; mapping field type. This is like any other field type, except that it expects you to assign a search document as the value. When you store data, the index processes this search document into an executable form and saves it for later.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.elastic.co/guide/en/elasticsearch/reference/6.2/query-dsl-percolate-query.html"&gt;Percolate Query&lt;/a&gt; takes one or more documents and limits results to ones whose stored searches match at least one document. When searching, the percolate query works like any other query element.&lt;/p&gt;

&lt;h1&gt;
  
  
  In Depth
&lt;/h1&gt;

&lt;p&gt;Under the hood, this is implemented in about the way you would expect: indexes with percolate fields keep a hidden (in memory) index. Documents listed in your percolate queries are first put in that index, then a normal query is executed against that index to see if the original percolate-field-bearing document matches.&lt;/p&gt;

&lt;p&gt;An important point to remember is that this hidden index gets its mappings from the original percolator index. So indexes used for percolate queries need to have mappings appropriate for the original data and the query document data.&lt;/p&gt;

&lt;p&gt;This introduces a bit of a management problem, in that your index data and the percolate query documents could use the same field in different ways. A simple answer to that is to use the &lt;a href="https://www.elastic.co/guide/en/elasticsearch/reference/6.2/object.html"&gt;object type&lt;/a&gt; to isolate the percolate-relevant mappings from normal document mappings.&lt;/p&gt;

&lt;p&gt;Assuming the queries you are using were originally written for another index of actual documents, it makes the most sense to isolate the data going directly into the percolate index and give the root level over to mapping definitions for percolate query documents.&lt;/p&gt;

&lt;p&gt;Also, because percolate fields are parsed into searches and saved at index time, you likely will need to reindex percolate documents after upgrading to take advantage of any optimizations to the system.&lt;/p&gt;

&lt;h1&gt;
  
  
  An Example
&lt;/h1&gt;

&lt;p&gt;In my opinion, percolator examples are one of the prime contributors to making the tool hard to understand. They tend to be too simple, to the point where it’s hard to distinguish the parts.&lt;/p&gt;

&lt;p&gt;In this example, we’re going to build out an index of saved term and price searches for toys. The idea behind it is that users should be able to put in a search term and a max price, then get notified as soon as something matching that term goes below this price. Users should also be able to turn these notifications on and off.&lt;/p&gt;

&lt;p&gt;The mapping below implements a percolate index to support this feature. Fields related to the saved search itself are in the &lt;code&gt;search&lt;/code&gt; object, while fields related to the original toys live at the root level of the mappings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mappings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"_doc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"percolator"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"boolean"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"float"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what a document that represents a stored search would look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"search"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"bool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"filter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; 
            &lt;/span&gt;&lt;span class="nl"&gt;"match"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; 
              &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nintendo switch"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"range"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"lte"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;300.00&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that we are only storing data inside the &lt;code&gt;search&lt;/code&gt; object field. The mappings for &lt;code&gt;price&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; are just there to support percolate queries.&lt;/p&gt;

&lt;p&gt;At query time, we want to use both the plain object fields and the “special” percolator field. This query would check, inside a user’s searches, to see which currently-enabled searches match the document.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"filter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"percolate"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search.query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"document"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Nintendo Switch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;250.00&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"term"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"search.enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"term"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"search.user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that it combines percolate matching of a document against the queries stored in the field with regular term queries to limit which documents we test based on their enabled state and the user id.&lt;/p&gt;

&lt;h1&gt;
  
  
  Some Additional Thoughts
&lt;/h1&gt;

&lt;p&gt;Because of the work involved in running queries as part of resolving a percolate filter, you might need to pay extra attention to shards/replicas for a percolate index. Each shard reduces the number of queries any one machine may have to run, by reducing the number of search-bearing documents to evaluate.&lt;/p&gt;

&lt;p&gt;Percolate queries have an option to get documents from another index inside the cluster. This takes the form of a literal GET request, so there’s not much benefit in trying to keep shards from the two indices on the same nodes.&lt;/p&gt;

</description>
      <category>elasticsearch</category>
    </item>
  </channel>
</rss>
