<?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: Rob Cox</title>
    <description>The latest articles on DEV Community by Rob Cox (@robbie_cox).</description>
    <link>https://dev.to/robbie_cox</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4211330%2F40e0274a-1d8a-4324-8f2f-75aa503362f5.png</url>
      <title>DEV Community: Rob Cox</title>
      <link>https://dev.to/robbie_cox</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/robbie_cox"/>
    <language>en</language>
    <item>
      <title>I built a database from scratch to show my kids how databases work</title>
      <dc:creator>Rob Cox</dc:creator>
      <pubDate>Tue, 27 Oct 2026 15:57:11 +0000</pubDate>
      <link>https://dev.to/robbie_cox/i-built-a-database-from-scratch-to-show-my-kids-how-databases-work-19a7</link>
      <guid>https://dev.to/robbie_cox/i-built-a-database-from-scratch-to-show-my-kids-how-databases-work-19a7</guid>
      <description>&lt;p&gt;Databases feel like magic. You type &lt;code&gt;SELECT * FROM cards WHERE name = 'Pikachu'&lt;/code&gt; and an answer comes back, instantly, from millions of rows. I've been coding for 30+ years and I still couldn't have explained to my kids what actually happens in between.&lt;/p&gt;

&lt;p&gt;So I built one. &lt;strong&gt;PyDB&lt;/strong&gt; is a small relational database written from scratch in Python, with every idea explained in plain English. It's not meant to replace Postgres. It's meant to be &lt;em&gt;read&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;You can try it in one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;fromscratch-pydb
pydb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Start with a table
&lt;/h2&gt;

&lt;p&gt;A table is just a grid: columns across the top, one row per thing. Let's make some trading cards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;pydb&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;power&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pydb&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Pikachu'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Electric'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pydb&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Raichu'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Electric'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pydb&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Charmander'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Fire'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behind that one line of SQL there's a whole pipeline. A &lt;strong&gt;tokenizer&lt;/strong&gt; chops the text into words (&lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;cards&lt;/code&gt;, &lt;code&gt;'Pikachu'&lt;/code&gt;), a &lt;strong&gt;parser&lt;/strong&gt; turns those words into a structure the program understands, and an &lt;strong&gt;executor&lt;/strong&gt; goes and fetches the rows. Each of those is its own small, readable Python file.&lt;/p&gt;

&lt;p&gt;Grouping works too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;pydb&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;power&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt;
&lt;span class="err"&gt;┌──────────┬──────────┬────────────┐&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt;     &lt;span class="err"&gt;│&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;│&lt;/span&gt; &lt;span class="k"&gt;AVG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;power&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;├──────────┼──────────┼────────────┤&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt; &lt;span class="n"&gt;Electric&lt;/span&gt; &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;│&lt;/span&gt; &lt;span class="n"&gt;Fire&lt;/span&gt;     &lt;span class="err"&gt;│&lt;/span&gt;        &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="err"&gt;│&lt;/span&gt;       &lt;span class="mi"&gt;52&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="err"&gt;│&lt;/span&gt;
&lt;span class="err"&gt;└──────────┴──────────┴────────────┘&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The back of the textbook
&lt;/h2&gt;

&lt;p&gt;Here's the question that got my kids interested: if a table has a million rows, how does the database find Pikachu without reading all of them?&lt;/p&gt;

&lt;p&gt;Ask the database how it &lt;em&gt;plans&lt;/em&gt; to find him:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;pydb&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Pikachu'&lt;/span&gt;
&lt;span class="k"&gt;Full&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;scan&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;full table scan&lt;/strong&gt; means checking every row, one by one. Fine for three cards, painful for a million.&lt;/p&gt;

&lt;p&gt;Now think about a school textbook. You don't read the whole book to find "Fossils". You flip to the &lt;strong&gt;index&lt;/strong&gt; at the back, see "page 55", and jump straight there. A database index is exactly that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;pydb&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_name&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pydb&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Pikachu'&lt;/span&gt;
&lt;span class="k"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;lookup&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="n"&gt;idx_name&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same question, completely different plan. The index in PyDB is a &lt;strong&gt;B-tree&lt;/strong&gt;, which I explain as a magical filing cabinet: each drawer holds a few cards in alphabetical order, and when a drawer gets too full it splits in two and puts a label on the cabinet saying which drawer to open. Because it always stays balanced, you reach any card in a handful of steps, even with millions of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  No take-backs (with a safety net)
&lt;/h2&gt;

&lt;p&gt;The idea my kids liked most was &lt;strong&gt;transactions&lt;/strong&gt;. Imagine trading cards with a friend: "I'll give you my Pikachu if you give me your Charmander." Both halves have to happen, or neither does. You don't want to hand over Pikachu and have your friend run off.&lt;/p&gt;

&lt;p&gt;A transaction groups several changes and promises all of them happen, or none do. If something goes wrong halfway, the database &lt;strong&gt;rolls back&lt;/strong&gt; as if nothing happened. In PyDB, every change in a transaction writes a little note saying how to undo itself, so rolling back means reading the notes backwards.&lt;/p&gt;

&lt;p&gt;Turn on the journal (&lt;code&gt;Database(path, journal=True)&lt;/code&gt;) and it goes one step further: every commit is written to disk &lt;em&gt;before&lt;/em&gt; it counts. Pull the plug mid-write, reopen the database, and it replays the journal. Your committed work is still there, and half-finished work leaves no trace.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in there
&lt;/h2&gt;

&lt;p&gt;Everything below is real, working code you can step through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL: &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt;, &lt;code&gt;CREATE&lt;/code&gt;/&lt;code&gt;DROP TABLE&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;JOIN&lt;/code&gt;s, &lt;code&gt;GROUP BY&lt;/code&gt; / &lt;code&gt;HAVING&lt;/code&gt;, subqueries and views&lt;/li&gt;
&lt;li&gt;B-tree indexes and a query planner (&lt;code&gt;EXPLAIN&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PRIMARY KEY&lt;/code&gt;, &lt;code&gt;NOT NULL&lt;/code&gt; and &lt;code&gt;UNIQUE&lt;/code&gt; constraints&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NULL&lt;/code&gt; with proper three-valued logic (TRUE / FALSE / UNKNOWN)&lt;/li&gt;
&lt;li&gt;Transactions with commit and rollback&lt;/li&gt;
&lt;li&gt;A crash-safe journal with checkpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's also tested the way I'd test production code: test-first, strictly typed, and with &lt;strong&gt;property-based tests&lt;/strong&gt; (using Hypothesis) that throw hundreds of random inputs at it looking for a rule it breaks. The docs include a page walking through one of those properties and the bug it catches.&lt;/p&gt;

&lt;h2&gt;
  
  
  What building it taught me
&lt;/h2&gt;

&lt;p&gt;The biggest surprise was how much of a "real" database is ordinary code. There's no single clever trick. It's a stack of small, understandable ideas, each solving one problem, and once you've built them yourself the magic turns into engineering. That's exactly what I wanted my kids to see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it, read it, break it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Install: &lt;code&gt;pip install fromscratch-pydb&lt;/code&gt;, then run &lt;code&gt;pydb&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Docs, with every concept explained simply: &lt;a href="https://robcox2910.github.io/pydb/" rel="noopener noreferrer"&gt;https://robcox2910.github.io/pydb/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/robcox2910/pydb" rel="noopener noreferrer"&gt;https://github.com/robcox2910/pydb&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PyDB is one of 18 projects in a series called &lt;strong&gt;From scratch in Python&lt;/strong&gt;. There's also an operating system, a programming language, git, a search engine, a tiny GPT and more, all tied together by PyStack. If you want the lot: &lt;code&gt;pip install fromscratch-pystack&lt;/code&gt;. The series home is &lt;a href="https://robcox2910.github.io/" rel="noopener noreferrer"&gt;https://robcox2910.github.io/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd love to hear what's confusing, what's missing, or which system you'd want built next.&lt;/p&gt;

</description>
      <category>python</category>
      <category>learning</category>
      <category>beginners</category>
      <category>database</category>
    </item>
  </channel>
</rss>
