Back to the library

Query plan, full scan

Variant: Full scan

A sequential scan: a cursor steps down every row at 280ms a row, and each read flashes to 55% clay so a row that was merely examined never reads as a row that matched. Two rows match and land in the result set, but the cursor still walks off the bottom and the meter fills all ten cells. Reach for it when you need to show why an unindexed predicate costs what it costs.

diagramdatabasesvgloop

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 — full scan</title>
    <style>
      :root {
        --ivory: #faf9f5;
        --slate: #141413;
        --clay: #d97757;
        --oat: #e3dacc;
        --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
        --cycle: 3.8s;
      }

      * {
        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 {
        fill: var(--oat);
      }

      .read,
      .hit {
        fill: var(--clay);
        opacity: 0;
      }

      /* Every row is read, one per 280ms, and the flash decays over 680ms —
         slow enough that two or three rows glow at once. The trailing wake is
         the point: a seq scan touches all of it, not one row in isolation.
         Reads peak at 55% so a row that was merely examined never looks like a
         row that matched. */
      .read {
        animation: read-flash var(--cycle) linear infinite;
      }

      .read:nth-child(1) {
        animation-delay: 0.3s;
      }
      .read:nth-child(2) {
        animation-delay: 0.58s;
      }
      .read:nth-child(3) {
        animation-delay: 0.86s;
      }
      .read:nth-child(4) {
        animation-delay: 1.14s;
      }
      .read:nth-child(5) {
        animation-delay: 1.42s;
      }
      .read:nth-child(6) {
        animation-delay: 1.7s;
      }
      .read:nth-child(7) {
        animation-delay: 1.98s;
      }
      .read:nth-child(8) {
        animation-delay: 2.26s;
      }
      .read:nth-child(9) {
        animation-delay: 2.54s;
      }
      .read:nth-child(10) {
        animation-delay: 2.82s;
      }

      @keyframes read-flash {
        0% {
          opacity: 0;
          animation-timing-function: var(--ease-out);
        }
        3.1% {
          opacity: 0.55;
        }
        17.9%,
        100% {
          opacity: 0;
        }
      }

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

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

      @keyframes hit-a {
        0%,
        22.6% {
          opacity: 0;
          animation-timing-function: var(--ease-out);
        }
        25.8%,
        90.8% {
          opacity: 1;
        }
        96.1%,
        100% {
          opacity: 0;
        }
      }

      @keyframes hit-b {
        0%,
        52.1% {
          opacity: 0;
          animation-timing-function: var(--ease-out);
        }
        55.2%,
        90.8% {
          opacity: 1;
        }
        96.1%,
        100% {
          opacity: 0;
        }
      }

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

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

      @keyframes probe-fade {
        0% {
          opacity: 0;
          animation-timing-function: var(--ease-out);
        }
        7.9%,
        81.6% {
          opacity: 1;
        }
        86.3%,
        100% {
          opacity: 0;
        }
      }

      /* steps(10) so the cursor lands on a row rather than sliding between
         them — a scan compares row by row, it does not glide. It walks off the
         end of the table, which is the whole cost of this plan. */
      .probe-step {
        animation: probe-walk var(--cycle) linear infinite;
      }

      @keyframes probe-walk {
        0%,
        7.9% {
          transform: translateY(0);
          animation-timing-function: steps(10);
        }
        81.6%,
        100% {
          transform: translateY(140px);
        }
      }

      .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%,
        26.9% {
          opacity: 0;
          transform: translateX(-14px) scale(0.96);
          animation-timing-function: var(--ease-out);
        }
        32.7%,
        90.8% {
          opacity: 1;
          transform: translateX(0) scale(1);
        }
        96.1%,
        100% {
          opacity: 0;
          transform: translateX(0) scale(0.97);
        }
      }

      @keyframes chip-b {
        0%,
        56.3% {
          opacity: 0;
          transform: translateX(-14px) scale(0.96);
          animation-timing-function: var(--ease-out);
        }
        62.1%,
        90.8% {
          opacity: 1;
          transform: translateX(0) scale(1);
        }
        96.1%,
        100% {
          opacity: 0;
          transform: translateX(0) scale(0.97);
        }
      }

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

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

      /* The cost meter. scaleX from zero is legitimate here — this is a
         determinate fill, not an element entering — and jump-start ticks the
         cell as the row is read, not one row late. Ten of ten. */
      .meter-fill {
        fill: var(--clay);
        transform-box: fill-box;
        transform-origin: left center;
        animation: meter-fill var(--cycle) linear infinite;
      }

      @keyframes meter-fill {
        0%,
        7.9% {
          transform: scaleX(0);
          opacity: 1;
          animation-timing-function: steps(10, jump-start);
        }
        81.6%,
        90.8% {
          transform: scaleX(1);
          opacity: 1;
        }
        96.1% {
          transform: scaleX(1);
          opacity: 0;
        }
        96.3%,
        100% {
          transform: scaleX(0);
          opacity: 0;
        }
      }

      @media (prefers-reduced-motion: reduce) {
        .probe-fade,
        .probe-step {
          animation: none;
          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%,
          26.9% {
            opacity: 0;
          }
          32.7%,
          90.8% {
            opacity: 1;
          }
          96.1%,
          100% {
            opacity: 0;
          }
        }

        @keyframes chip-still-b {
          0%,
          56.3% {
            opacity: 0;
          }
          62.1%,
          90.8% {
            opacity: 1;
          }
          96.1%,
          100% {
            opacity: 0;
          }
        }

        /* No travel: the meter just states the cost — every row was read. */
        .meter-fill {
          animation: none;
          transform: scaleX(1);
        }
      }
    </style>
  </head>
  <body>
    <svg
      viewBox="0 0 280 200"
      role="img"
      aria-label="A sequential scan reading every row of a table to find two matches"
    >
      <g>
        <rect class="row" x="64" y="24" width="112" height="10" rx="2" />
        <rect class="row" x="64" y="38" width="112" height="10" rx="2" />
        <rect class="row" x="64" y="52" width="112" height="10" rx="2" />
        <rect class="row" x="64" y="66" width="112" height="10" rx="2" />
        <rect class="row" x="64" y="80" width="112" height="10" rx="2" />
        <rect class="row" x="64" y="94" width="112" height="10" rx="2" />
        <rect class="row" x="64" y="108" width="112" height="10" rx="2" />
        <rect class="row" x="64" y="122" width="112" height="10" rx="2" />
        <rect class="row" x="64" y="136" width="112" height="10" rx="2" />
        <rect class="row" x="64" y="150" width="112" height="10" rx="2" />
      </g>

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

      <rect class="hit hit-a" x="64" y="52" width="112" height="10" rx="2" />
      <rect class="hit hit-b" x="64" y="108" width="112" height="10" rx="2" />

      <g class="probe">
        <g class="probe-fade">
          <g class="probe-step">
            <path d="M46 24 L58 29 L46 34 Z" />
          </g>
        </g>
      </g>

      <path class="feed" d="M180 85 L192 85" />
      <rect class="slot" x="194" y="64" width="40" height="42" rx="4" />
      <rect class="chip chip-a" x="200" y="72" width="28" height="10" rx="2" />
      <rect class="chip chip-b" x="200" y="88" width="28" height="10" rx="2" />

      <rect class="meter-track" x="64" y="176" width="112" height="5" rx="2.5" />
      <rect class="meter-fill" x="64" y="176" width="112" height="5" rx="2.5" />
      <g class="meter-tick">
        <rect x="75.2" y="176" width="1.6" height="5" />
        <rect x="86.4" y="176" width="1.6" height="5" />
        <rect x="97.6" y="176" width="1.6" height="5" />
        <rect x="108.8" y="176" width="1.6" height="5" />
        <rect x="120" y="176" width="1.6" height="5" />
        <rect x="131.2" y="176" width="1.6" height="5" />
        <rect x="142.4" y="176" width="1.6" height="5" />
        <rect x="153.6" y="176" width="1.6" height="5" />
        <rect x="164.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.