Back to the library

Query plan, index seek

Variant: Index seek

The same table with an index over it. The probe descends three B-tree nodes — root, branch, leaf — with a beat of stillness at each, because a seek is compare then descend, not a glide. It then fetches two rows that sit nowhere near each other, since index order is not heap order. Five reads on the same meter that the full scan fills to ten.

diagramdatabasesvg

The source

A complete HTML document. Paste it into an empty .html file and it plays — no stylesheet, script, font, or image to fetch.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Query plan — index seek</title>
    <style>
      :root {
        --ivory: #faf9f5;
        --slate: #141413;
        --clay: #d97757;
        --oat: #e3dacc;
        --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
        --cycle: 3.2s;
      }

      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
      }

      body {
        min-height: 100vh;
        display: grid;
        place-items: center;
        background: var(--ivory);
        font-family:
          system-ui,
          -apple-system,
          'Segoe UI',
          Roboto,
          sans-serif;
        color: var(--slate);
      }

      svg {
        width: 280px;
        max-width: 100%;
        height: auto;
        display: block;
      }

      .row,
      .node {
        fill: var(--oat);
      }

      .edge {
        stroke: var(--oat);
        stroke-width: 1.5;
        fill: none;
      }

      /* Each traversed edge draws itself over the same 180ms the probe takes to
         travel it, so the line is the probe's own path, not decoration. */
      .edge-live {
        stroke: var(--clay);
        stroke-width: 1.5;
        fill: none;
        stroke-dasharray: 1;
        stroke-dashoffset: 1;
      }

      .edge-1 {
        animation: draw-1 var(--cycle) linear infinite;
      }

      .edge-2 {
        animation: draw-2 var(--cycle) linear infinite;
      }

      .edge-3 {
        animation: draw-3 var(--cycle) linear infinite;
      }

      @keyframes draw-1 {
        0%,
        19.3% {
          stroke-dashoffset: 1;
          opacity: 1;
          animation-timing-function: var(--ease-out);
        }
        25%,
        90.3% {
          stroke-dashoffset: 0;
          opacity: 1;
        }
        96.6%,
        100% {
          stroke-dashoffset: 0;
          opacity: 0;
        }
      }

      @keyframes draw-2 {
        0%,
        30% {
          stroke-dashoffset: 1;
          opacity: 1;
          animation-timing-function: var(--ease-out);
        }
        35.6%,
        90.3% {
          stroke-dashoffset: 0;
          opacity: 1;
        }
        96.6%,
        100% {
          stroke-dashoffset: 0;
          opacity: 0;
        }
      }

      @keyframes draw-3 {
        0%,
        40.6% {
          stroke-dashoffset: 1;
          opacity: 1;
          animation-timing-function: var(--ease-out);
        }
        46.9%,
        90.3% {
          stroke-dashoffset: 0;
          opacity: 1;
        }
        96.6%,
        100% {
          stroke-dashoffset: 0;
          opacity: 0;
        }
      }

      .lit {
        fill: var(--clay);
        opacity: 0;
      }

      .lit-root {
        animation: lit-root var(--cycle) linear infinite;
      }

      .lit-branch {
        animation: lit-branch var(--cycle) linear infinite;
      }

      .lit-leaf {
        animation: lit-leaf var(--cycle) linear infinite;
      }

      .lit-row-a {
        animation: lit-row-a var(--cycle) linear infinite;
      }

      .lit-row-b {
        animation: lit-row-b var(--cycle) linear infinite;
      }

      @keyframes lit-root {
        0%,
        14.3% {
          opacity: 0;
          animation-timing-function: var(--ease-out);
        }
        18.2%,
        90.3% {
          opacity: 1;
        }
        96.6%,
        100% {
          opacity: 0;
        }
      }

      @keyframes lit-branch {
        0%,
        25% {
          opacity: 0;
          animation-timing-function: var(--ease-out);
        }
        28.8%,
        90.3% {
          opacity: 1;
        }
        96.6%,
        100% {
          opacity: 0;
        }
      }

      @keyframes lit-leaf {
        0%,
        35.6% {
          opacity: 0;
          animation-timing-function: var(--ease-out);
        }
        39.4%,
        90.3% {
          opacity: 1;
        }
        96.6%,
        100% {
          opacity: 0;
        }
      }

      @keyframes lit-row-a {
        0%,
        46.9% {
          opacity: 0;
          animation-timing-function: var(--ease-out);
        }
        51.2%,
        90.3% {
          opacity: 1;
        }
        96.6%,
        100% {
          opacity: 0;
        }
      }

      @keyframes lit-row-b {
        0%,
        63.1% {
          opacity: 0;
          animation-timing-function: var(--ease-out);
        }
        66.8%,
        90.3% {
          opacity: 1;
        }
        96.6%,
        100% {
          opacity: 0;
        }
      }

      .probe {
        fill: var(--clay);
      }

      .probe-fade {
        animation: probe-fade var(--cycle) linear infinite;
      }

      @keyframes probe-fade {
        0%,
        9.4% {
          opacity: 0;
          animation-timing-function: var(--ease-out);
        }
        13.7%,
        75% {
          opacity: 1;
        }
        79.4%,
        100% {
          opacity: 0;
        }
      }

      /* Three hops of 180ms with a beat of stillness at each node. The pauses
         are the seek: compare, choose a child, descend. The second fetch jumps
         to a row further up the table, not the next one down: adjacent index
         entries point wherever their rows happen to live. */
      .probe-move {
        animation: descend var(--cycle) linear infinite;
      }

      @keyframes descend {
        0%,
        19.3% {
          transform: translate(0px, 0px);
          animation-timing-function: var(--ease-out);
        }
        25%,
        30% {
          transform: translate(23px, 38px);
          animation-timing-function: var(--ease-out);
        }
        35.6%,
        40.6% {
          transform: translate(45px, 75px);
          animation-timing-function: var(--ease-out);
        }
        46.9%,
        58.1% {
          transform: translate(68px, 74px);
          animation-timing-function: var(--ease-out);
        }
        63.1%,
        100% {
          transform: translate(68px, 32px);
        }
      }

      .feed {
        stroke: var(--oat);
        stroke-width: 1.5;
        fill: none;
      }

      .slot {
        fill: none;
        stroke: var(--slate);
        stroke-width: 1;
        stroke-dasharray: 3 3;
        opacity: 0.22;
      }

      .chip {
        fill: var(--clay);
        opacity: 0;
        transform-box: fill-box;
        transform-origin: center;
      }

      .chip-a {
        animation: chip-a var(--cycle) linear infinite;
      }

      .chip-b {
        animation: chip-b var(--cycle) linear infinite;
      }

      @keyframes chip-a {
        0%,
        48.8% {
          opacity: 0;
          transform: translateX(-14px) scale(0.96);
          animation-timing-function: var(--ease-out);
        }
        55.7%,
        90.3% {
          opacity: 1;
          transform: translateX(0) scale(1);
        }
        96.6%,
        100% {
          opacity: 0;
          transform: translateX(0) scale(0.97);
        }
      }

      @keyframes chip-b {
        0%,
        65% {
          opacity: 0;
          transform: translateX(-14px) scale(0.96);
          animation-timing-function: var(--ease-out);
        }
        71.8%,
        90.3% {
          opacity: 1;
          transform: translateX(0) scale(1);
        }
        96.6%,
        100% {
          opacity: 0;
          transform: translateX(0) scale(0.97);
        }
      }

      .meter-track {
        fill: var(--oat);
      }

      .meter-tick {
        fill: var(--ivory);
      }

      /* Same ten-cell meter as the other two plans, and the same unit: rows of
         this table touched. Two. The descent above is real work, but index
         nodes are not table rows, and counting both on one meter would make
         the three plans incomparable — and would land this plan on the same
         five cells as the early exit. steps(1) holds, then ticks on the fetch. */
      .meter-fill {
        fill: var(--clay);
        transform-box: fill-box;
        transform-origin: left center;
        animation: meter-seek var(--cycle) linear infinite;
      }

      @keyframes meter-seek {
        0% {
          transform: scaleX(0);
          opacity: 1;
          animation-timing-function: steps(1, jump-end);
        }
        46.9% {
          transform: scaleX(0.1);
          animation-timing-function: steps(1, jump-end);
        }
        63.1%,
        90.3% {
          transform: scaleX(0.2);
          opacity: 1;
        }
        96.6% {
          transform: scaleX(0.2);
          opacity: 0;
        }
        96.8%,
        100% {
          transform: scaleX(0);
          opacity: 0;
        }
      }

      @media (prefers-reduced-motion: reduce) {
        .probe-fade,
        .probe-move {
          animation: none;
          opacity: 0;
        }

        /* Path stays readable without the draw-on: the edges just appear. */
        .edge-live {
          stroke-dashoffset: 0;
          opacity: 0;
        }

        .edge-1 {
          animation: edge-still-1 var(--cycle) linear infinite;
        }

        .edge-2 {
          animation: edge-still-2 var(--cycle) linear infinite;
        }

        .edge-3 {
          animation: edge-still-3 var(--cycle) linear infinite;
        }

        @keyframes edge-still-1 {
          0%,
          19.3% {
            opacity: 0;
          }
          25%,
          90.3% {
            opacity: 1;
          }
          96.6%,
          100% {
            opacity: 0;
          }
        }

        @keyframes edge-still-2 {
          0%,
          30% {
            opacity: 0;
          }
          35.6%,
          90.3% {
            opacity: 1;
          }
          96.6%,
          100% {
            opacity: 0;
          }
        }

        @keyframes edge-still-3 {
          0%,
          40.6% {
            opacity: 0;
          }
          46.9%,
          90.3% {
            opacity: 1;
          }
          96.6%,
          100% {
            opacity: 0;
          }
        }

        .chip-a {
          animation: chip-still-a var(--cycle) linear infinite;
        }

        .chip-b {
          animation: chip-still-b var(--cycle) linear infinite;
        }

        @keyframes chip-still-a {
          0%,
          48.8% {
            opacity: 0;
          }
          55.7%,
          90.3% {
            opacity: 1;
          }
          96.6%,
          100% {
            opacity: 0;
          }
        }

        @keyframes chip-still-b {
          0%,
          65% {
            opacity: 0;
          }
          71.8%,
          90.3% {
            opacity: 1;
          }
          96.6%,
          100% {
            opacity: 0;
          }
        }

        /* Cost stated, not travelled: two of ten rows touched. */
        .meter-fill {
          animation: none;
          transform: scaleX(0.2);
        }
      }
    </style>
  </head>
  <body>
    <svg
      viewBox="0 0 280 200"
      role="img"
      aria-label="An index seek descending three nodes of a B-tree, then fetching the two rows it points at: two of ten rows touched"
    >
      <g class="edge">
        <path d="M48 46 L25 70" />
        <path d="M48 46 L71 70" />
        <path d="M25 84 L15 108" />
        <path d="M25 84 L41 108" />
        <path d="M71 84 L67 108" />
        <path d="M71 84 L93 108" />
      </g>

      <g>
        <rect class="node" x="26" y="32" width="44" height="14" rx="3" />
        <rect class="node" x="6" y="70" width="38" height="14" rx="3" />
        <rect class="node" x="52" y="70" width="38" height="14" rx="3" />
        <rect class="node" x="4" y="108" width="22" height="12" rx="3" />
        <rect class="node" x="30" y="108" width="22" height="12" rx="3" />
        <rect class="node" x="56" y="108" width="22" height="12" rx="3" />
        <rect class="node" x="82" y="108" width="22" height="12" rx="3" />
      </g>

      <g>
        <rect class="row" x="124" y="24" width="92" height="10" rx="2" />
        <rect class="row" x="124" y="38" width="92" height="10" rx="2" />
        <rect class="row" x="124" y="52" width="92" height="10" rx="2" />
        <rect class="row" x="124" y="66" width="92" height="10" rx="2" />
        <rect class="row" x="124" y="80" width="92" height="10" rx="2" />
        <rect class="row" x="124" y="94" width="92" height="10" rx="2" />
        <rect class="row" x="124" y="108" width="92" height="10" rx="2" />
        <rect class="row" x="124" y="122" width="92" height="10" rx="2" />
        <rect class="row" x="124" y="136" width="92" height="10" rx="2" />
        <rect class="row" x="124" y="150" width="92" height="10" rx="2" />
      </g>

      <path class="edge-live edge-1" pathLength="1" d="M48 46 L71 70" />
      <path class="edge-live edge-2" pathLength="1" d="M71 84 L93 108" />
      <path class="edge-live edge-3" pathLength="1" d="M104 114 L124 113" />

      <rect class="lit lit-root" x="26" y="32" width="44" height="14" rx="3" />
      <rect class="lit lit-branch" x="52" y="70" width="38" height="14" rx="3" />
      <rect class="lit lit-leaf" x="82" y="108" width="22" height="12" rx="3" />
      <rect class="lit lit-row-a" x="124" y="108" width="92" height="10" rx="2" />
      <rect class="lit lit-row-b" x="124" y="66" width="92" height="10" rx="2" />

      <g class="probe-fade">
        <g class="probe-move">
          <circle class="probe" cx="48" cy="39" r="4.5" />
        </g>
      </g>

      <path class="feed" d="M216 85 L228 85" />
      <rect class="slot" x="230" y="64" width="40" height="42" rx="4" />
      <rect class="chip chip-a" x="236" y="72" width="28" height="10" rx="2" />
      <rect class="chip chip-b" x="236" y="88" width="28" height="10" rx="2" />

      <rect class="meter-track" x="124" y="176" width="92" height="5" rx="2.5" />
      <rect class="meter-fill" x="124" y="176" width="92" height="5" rx="2.5" />
      <g class="meter-tick">
        <rect x="133.2" y="176" width="1.6" height="5" />
        <rect x="142.4" y="176" width="1.6" height="5" />
        <rect x="151.6" y="176" width="1.6" height="5" />
        <rect x="160.8" y="176" width="1.6" height="5" />
        <rect x="170" y="176" width="1.6" height="5" />
        <rect x="179.2" y="176" width="1.6" height="5" />
        <rect x="188.4" y="176" width="1.6" height="5" />
        <rect x="197.6" y="176" width="1.6" height="5" />
        <rect x="206.8" y="176" width="1.6" height="5" />
      </g>
    </svg>
  </body>
</html>

The variant family

Same idea, one axis moved. Compare them side by side before you commit.