<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Primary-Keys on EXPLAIN ANALYZE</title>
        <link>https://explainanalyze.com/tags/primary-keys/</link>
        <description>Recent content in Primary-Keys on EXPLAIN ANALYZE</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en-us</language>
        <lastBuildDate>Wed, 22 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://explainanalyze.com/tags/primary-keys/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>Random UUIDs as Primary Keys: The B-Tree Penalty</title>
            <link>https://explainanalyze.com/p/random-uuids-as-primary-keys-the-b-tree-penalty/</link>
            <pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate>
            <guid>https://explainanalyze.com/p/random-uuids-as-primary-keys-the-b-tree-penalty/</guid>
            <description>&lt;img src=&#34;https://explainanalyze.com/&#34; alt=&#34;Featured image of post Random UUIDs as Primary Keys: The B-Tree Penalty&#34; /&gt;&lt;div class=&#34;tldr-box&#34;&gt;&#xA;    &lt;strong&gt;TL;DR&lt;/strong&gt;&#xA;    &lt;div&gt;UUIDv4 primary keys are globally unique and coordination-free, and the cost is paid every time you write a row: random B-tree positions, page splits, secondary indexes bloated with 16- or 36-byte key copies, and a working set that stops fitting in the buffer pool once the table is large enough. UUIDv7 fixes the insert-locality problem (time-ordered, sortable) without changing storage size; the full fix is picking v7, storing as &lt;code&gt;BINARY(16)&lt;/code&gt; or native &lt;code&gt;uuid&lt;/code&gt;, and keeping UUIDs at the API boundary rather than internal to every join.&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&#xA;&lt;p&gt;A table configured like this on day one looks unremarkable:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;CREATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TABLE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;orders&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;CHAR&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;36&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;PRIMARY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;KEY&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;-- UUIDv4, generated by the application&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;Inserts are fast, reads are fast, the ORM is happy. At 100,000 rows, it&amp;rsquo;s still fine. At 10 million, the nightly ingest job gets noticeably slower. At 200 million, inserts take 50 ms each instead of 2 ms, the buffer pool is constantly churning, and the secondary indexes are three to four times the size they&amp;rsquo;d be with a &lt;code&gt;BIGINT&lt;/code&gt; primary key. Nothing about the schema changed. The table just got large enough for a design decision to start charging rent.&lt;/p&gt;&#xA;&lt;p&gt;The obvious fix is &amp;ldquo;use BIGINT auto-increment.&amp;rdquo; That&amp;rsquo;s the right answer in a lot of cases and the wrong one in others — it reintroduces coordination requirements, leaks row counts through URL-exposed IDs, and doesn&amp;rsquo;t work for schemas that need to be generated offline or across shards. UUIDs exist because those constraints are real. The sharper question is: what exactly is UUIDv4 costing you at scale, and which of those costs have cheaper alternatives?&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-random-keys-do-to-a-b-tree&#34;&gt;What random keys do to a B-tree&#xA;&lt;/h2&gt;&lt;p&gt;B-tree indexes are sorted structures. When the primary key is an auto-incrementing integer, every new row goes to the end — the rightmost leaf page is the only one that gets written to, and the rest of the index stays in cache undisturbed. Inserts are sequential and cheap.&lt;/p&gt;&#xA;&lt;p&gt;UUIDv4 is random by design. Every new row lands at a random position in the B-tree. Instead of appending to one page, the engine has to:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Find the right page somewhere in the middle of the tree.&lt;/li&gt;&#xA;&lt;li&gt;Load it into the buffer pool if it isn&amp;rsquo;t already (on a large table, it usually isn&amp;rsquo;t).&lt;/li&gt;&#xA;&lt;li&gt;Split it if it&amp;rsquo;s full.&lt;/li&gt;&#xA;&lt;li&gt;Write both halves back.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;On a table with hundreds of millions of rows, the index doesn&amp;rsquo;t fit in memory, so most inserts trigger a random disk read before they can do anything else. The write amplification is real and measurable — factor of 5 to 10× versus sequential inserts isn&amp;rsquo;t unusual.&lt;/p&gt;&#xA;&lt;p&gt;The damage doesn&amp;rsquo;t stop at the primary-key index. In InnoDB (MySQL), every secondary index includes a copy of the primary key at its leaves. A 36-byte &lt;code&gt;CHAR(36)&lt;/code&gt; UUID embedded in every secondary index entry means larger indexes, more pages, more I/O — compared to an 8-byte &lt;code&gt;BIGINT&lt;/code&gt;. Secondary indexes on a UUID-keyed table are routinely 3–4× the size of the same indexes on a &lt;code&gt;BIGINT&lt;/code&gt;-keyed table. Every lookup through a secondary index reads more pages to cover the same rows.&lt;/p&gt;&#xA;&lt;p&gt;PostgreSQL handles storage differently — its heap means the primary key is just another index, so the physical table isn&amp;rsquo;t ordered by it. But the primary-key index still suffers the same random-insertion pathology, and the write amplification from random page loads still applies.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Page splits compound over time.&lt;/strong&gt; When a new UUID lands in a full page, InnoDB splits the page in two, each roughly half full. Over millions of inserts, the index develops internal fragmentation — pages allocated but only partially used. The index is physically larger than it needs to be, and scans read more pages for the same row count. &lt;code&gt;OPTIMIZE TABLE&lt;/code&gt; (MySQL) or &lt;code&gt;REINDEX&lt;/code&gt; (PostgreSQL) can repack the index, but on a busy table it&amp;rsquo;s a maintenance window you have to schedule.&lt;/p&gt;&#xA;&lt;h2 id=&#34;uuidv7-the-insert-locality-fix&#34;&gt;UUIDv7: the insert-locality fix&#xA;&lt;/h2&gt;&lt;p&gt;UUIDv7 is the version most new code should reach for when UUIDs are the right answer. It encodes a Unix millisecond timestamp into the high 48 bits, with random bits filling the rest. Two practical consequences:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Sortable.&lt;/strong&gt; Sequential generation means new IDs land at the end of the B-tree, not scattered across it. Insert locality is close to a &lt;code&gt;BIGINT&lt;/code&gt;&amp;rsquo;s. The pathological page-split behaviour of v4 goes away.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Time-parseable.&lt;/strong&gt; The creation time is embedded in the ID, recoverable from the primary key alone — useful for log correlation, rough time-range filtering, and debugging without reaching for &lt;code&gt;created_at&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt; 1&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 2&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 3&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 4&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 5&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 6&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 7&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 8&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 9&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;10&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;11&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;12&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;13&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;14&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- UUIDv7: time-ordered, so inserts are roughly sequential&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- PostgreSQL 18 ships a built-in uuidv7() function&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;CREATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TABLE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;orders&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;UUID&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;PRIMARY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;KEY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;DEFAULT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;uuidv7&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- Recover creation time from the ID — no created_at column needed&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;       &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;uuid_extract_timestamp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;AS&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;created_at&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;       &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;uuid_extract_timestamp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)::&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;date&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;AS&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;created_date&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;orders&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ORDER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;BY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;DESC&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;-- v7 sorts chronologically, newest first&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;LIMIT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;&lt;code&gt;uuid_extract_timestamp()&lt;/code&gt; has existed in PostgreSQL since 17 but only returned a value for UUIDv1. PG 18 extended it to support v7 alongside the new &lt;code&gt;uuidv7()&lt;/code&gt; generator. One caveat: calling it in a &lt;code&gt;WHERE&lt;/code&gt; clause (&lt;code&gt;WHERE uuid_extract_timestamp(id) &amp;gt;= &#39;2026-04-01&#39;&lt;/code&gt;) is non-SARGable and forces a scan — see &lt;a class=&#34;link&#34; href=&#34;https://explainanalyze.com/p/non-sargable-predicates-how-a-function-in-where-kills-your-index/&#34; &gt;Non-SARGable Predicates&lt;/a&gt;. For indexed time-range filtering, keep a &lt;code&gt;created_at&lt;/code&gt; column as the query target, or compare against a boundary UUID generated at the target timestamp.&lt;/p&gt;&#xA;&lt;p&gt;MySQL 8 doesn&amp;rsquo;t ship a v7 generator or a timestamp extractor, so application-side generation is the norm there — libraries exist in every major language, and most modern ORMs default to v7 if you ask for UUIDs. Extraction is manual: for &lt;code&gt;BINARY(16)&lt;/code&gt; storage (the recommended form), the first 6 bytes hold the millisecond timestamp.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;6&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;7&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- MySQL: manually parse v7&amp;#39;s timestamp prefix (BINARY(16) storage)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;       &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;FROM_UNIXTIME&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CONV&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;HEX&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SUBSTRING&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;6&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;16&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1000&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;AS&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;created_at&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;       &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;DATE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;FROM_UNIXTIME&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;CONV&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;HEX&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;SUBSTRING&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;6&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;16&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1000&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;AS&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;created_date&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;orders&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ORDER&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;BY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;DESC&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;-- v7 sorts chronologically&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;LIMIT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;For &lt;code&gt;CHAR(36)&lt;/code&gt; storage, the extraction strips hyphens first: &lt;code&gt;CONCAT(SUBSTRING(id, 1, 8), SUBSTRING(id, 10, 4))&lt;/code&gt; gives the 12 hex characters of the timestamp prefix. If your v1 UUIDs were stored with &lt;code&gt;UUID_TO_BIN(id, 1)&lt;/code&gt; (the swap flag that reorders bytes for v1 index locality), the byte layout differs and the substring offsets change. Most v7-generating libraries skip the swap because v7 is already time-ordered without it — check what yours does before trusting the extraction.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;What v7 doesn&amp;rsquo;t change.&lt;/strong&gt; It&amp;rsquo;s still 16 bytes on disk, and still 36 if you stored it as &lt;code&gt;CHAR(36)&lt;/code&gt; — the insert-locality win doesn&amp;rsquo;t come with a storage discount, so the overhead versus a &lt;code&gt;BIGINT&lt;/code&gt; is the same as v4. The readable creation timestamp is usually a feature and occasionally a problem: in systems where row-creation time is sensitive (order IDs revealing traffic patterns to competitors, user IDs exposing signup timing), it&amp;rsquo;s the one property v4 had that v7 gives up.&lt;/p&gt;&#xA;&lt;div class=&#34;note-box&#34;&gt;&#xA;    &lt;strong&gt;CHAR(36) is the silent tax&lt;/strong&gt;&#xA;    &lt;div&gt;The worst-case UUID storage — &lt;code&gt;CHAR(36)&lt;/code&gt; — is what most ORM-generated schemas default to, because it&amp;rsquo;s the portable representation. &lt;code&gt;BINARY(16)&lt;/code&gt; in MySQL or the native &lt;code&gt;uuid&lt;/code&gt; type in PostgreSQL cuts storage by more than half and keeps comparisons on fixed-width integers instead of strings. Pick the narrow form on day one; retrofitting it later is a full-table rewrite that touches every secondary index.&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&#xA;&lt;h2 id=&#34;uuid-to-integer-mapping-keep-uuids-at-the-edge&#34;&gt;UUID-to-integer mapping: keep UUIDs at the edge&#xA;&lt;/h2&gt;&lt;p&gt;The other workable fix is structural: expose UUIDs externally, use integers internally. A single lookup table maps the external UUID to an internal &lt;code&gt;BIGINT&lt;/code&gt;, and every other table in the database uses the &lt;code&gt;BIGINT&lt;/code&gt; as its foreign key. The UUID lookup happens once — at the API boundary — and everything downstream is fast, compact, 8-byte integer joins.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt; 1&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 2&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 3&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 4&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 5&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 6&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 7&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 8&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 9&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;10&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;11&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;12&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;13&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;14&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;15&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;16&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;CREATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TABLE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;users&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;BIGINT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;AUTO_INCREMENT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;PRIMARY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;KEY&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;external_id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;BINARY&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;16&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;UNIQUE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;-- the UUID the outside world sees&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- Every other table references the BIGINT, not the UUID&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;CREATE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;TABLE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;orders&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;BIGINT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;AUTO_INCREMENT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;PRIMARY&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;KEY&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;user_id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;BIGINT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NOT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;REFERENCES&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;users&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- API request comes in with a UUID — one indexed lookup to resolve it&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;users&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;external_id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;UUID_TO_BIN&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;a1b2c3d4-...&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;-- From here on, everything uses the BIGINT&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;The UUID column has a unique index, so the lookup is a single index seek — sub-millisecond regardless of table size. The rest of the schema gets 8-byte keys everywhere: smaller indexes, faster joins, no page splits, no secondary-index bloat. The external-facing API still uses UUIDs, so you don&amp;rsquo;t leak sequence information or row counts.&lt;/p&gt;&#xA;&lt;p&gt;The trade-off is an extra layer of indirection. Every inbound request resolves the UUID before anything else; in practice this is negligible (one indexed lookup), but it means the schema has two identity systems to maintain. For long-lived OLTP applications where every join on every table pays the UUID cost, this structure is often worth the extra lookup.&lt;/p&gt;&#xA;&lt;h2 id=&#34;when-random-uuids-are-actually-fine&#34;&gt;When random UUIDs are actually fine&#xA;&lt;/h2&gt;&lt;p&gt;Not every schema needs to bend. Three cases where UUIDv4 as a primary key is a defensible choice:&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Small tables that stay small.&lt;/strong&gt; A configuration table, a lookup table, a feature-flag table. At 50,000 rows the page-split pathology doesn&amp;rsquo;t show up, secondary indexes are tiny, and the convenience of client-generated IDs outweighs any cost.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Write rates low enough that random I/O doesn&amp;rsquo;t matter.&lt;/strong&gt; An admin tool recording 50 events per minute doesn&amp;rsquo;t care about write amplification. The index fits in cache, every page is warm, page splits happen rarely enough that fragmentation stays manageable. &amp;ldquo;Doesn&amp;rsquo;t survive scale&amp;rdquo; is only a problem at scale.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Information-leak concerns that outweigh performance.&lt;/strong&gt; If hiding creation-order is a hard requirement (competitive, privacy, or security), v7&amp;rsquo;s embedded timestamp is a non-starter and v4 is the only UUID version that meets the requirement. Pay the write-amplification cost and use the UUID-to-integer mapping to contain the damage.&lt;/p&gt;&#xA;&lt;h2 id=&#34;why-v4-keeps-showing-up-as-the-default&#34;&gt;Why v4 keeps showing up as the default&#xA;&lt;/h2&gt;&lt;p&gt;Schema-reading assistants and scaffolding tools reinforce UUIDv4 as the default answer, and the reason is mostly inertial. Training corpora are heavy on examples where &lt;code&gt;uuid4()&lt;/code&gt; is the canonical &amp;ldquo;globally unique ID&amp;rdquo; call; &lt;code&gt;CREATE TABLE ... id UUID DEFAULT gen_random_uuid()&lt;/code&gt; appears in orders of magnitude more tutorials than the v7 equivalent. Asked for a new table schema, a model produces the v4 version because that&amp;rsquo;s what the surrounding code it learned from produced. B-tree locality and write amplification don&amp;rsquo;t show up in the DDL — they&amp;rsquo;re runtime properties of the key distribution — so the catalog gives no signal that v4 and v7 behave differently at 100M rows. Both look identical in &lt;code&gt;information_schema&lt;/code&gt;: &lt;code&gt;uuid&lt;/code&gt; or &lt;code&gt;char(36)&lt;/code&gt;, primary key, not null.&lt;/p&gt;&#xA;&lt;p&gt;The fix is the same discipline this post already makes the case for, with one amplified beat: document the choice where a schema reader can find it. A comment on the PK column (&lt;code&gt;&#39;UUIDv7 — time-ordered; required for insert locality&#39;&lt;/code&gt; or &lt;code&gt;&#39;UUIDv4 — randomized; chosen to hide creation order&#39;&lt;/code&gt;) turns a silent convention into a machine-readable decision. The next reader — teammate or model — sees why the column is what it is and why the alternative was rejected. Without the comment, the next table scaffolded by an assistant inherits whichever version the training data sampled, and the schema drifts toward whichever default produces the most plausible-looking DDL.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-bigger-picture&#34;&gt;The bigger picture&#xA;&lt;/h2&gt;&lt;p&gt;UUIDv4 is a tool that solved a coordination problem — distributed ID generation without central authority — and accidentally became the default for everything, including the cases where coordination wasn&amp;rsquo;t a problem and the cost of random writes is non-trivial. &amp;ldquo;Pick a UUID for your PK&amp;rdquo; is a decision most schemas make without ever being explicit about what they&amp;rsquo;re trading.&lt;/p&gt;&#xA;&lt;p&gt;The decision matrix is short. Do you need globally unique, coordination-free IDs? If no, use &lt;code&gt;BIGINT&lt;/code&gt;. If yes, use UUIDv7 and store it as &lt;code&gt;BINARY(16)&lt;/code&gt; or native &lt;code&gt;uuid&lt;/code&gt; — never &lt;code&gt;CHAR(36)&lt;/code&gt;. If v7&amp;rsquo;s embedded timestamp is a problem, use v4 but keep it at the API boundary and use integers inside the schema. Each of those decisions costs almost nothing on day one and saves a lot of rework at 100 million rows — which is the point where &amp;ldquo;UUIDs as primary keys&amp;rdquo; stops being a default and starts being a choice with real consequences.&lt;/p&gt;&#xA;</description>
        </item><item>
            <title>The Bare `id` Primary Key: When Every Table Joins to Every Other Table</title>
            <link>https://explainanalyze.com/p/the-bare-id-primary-key-when-every-table-joins-to-every-other-table/</link>
            <pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate>
            <guid>https://explainanalyze.com/p/the-bare-id-primary-key-when-every-table-joins-to-every-other-table/</guid>
            <description>&lt;img src=&#34;https://explainanalyze.com/&#34; alt=&#34;Featured image of post The Bare `id` Primary Key: When Every Table Joins to Every Other Table&#34; /&gt;&lt;div class=&#34;tldr-box&#34;&gt;&#xA;    &lt;strong&gt;TL;DR&lt;/strong&gt;&#xA;    &lt;div&gt;A bare &lt;code&gt;id&lt;/code&gt; primary key on every table makes &lt;code&gt;a.id = b.id&lt;/code&gt; valid SQL between any two tables, which means neither a human reviewing the query nor an LLM generating one can tell which of those equalities are meaningful. The fix isn&amp;rsquo;t picking the &amp;ldquo;right&amp;rdquo; PK type — it&amp;rsquo;s naming primary keys after the table they identify, so the schema describes its own relationships.&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&#xA;&lt;p&gt;Here&amp;rsquo;s a query an AI assistant generated against a real production schema:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;&#xA;&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3&#xA;&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4&#xA;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&#xA;&lt;td class=&#34;lntd&#34;&gt;&#xA;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;SELECT&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;u&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;email&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;payload&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;users&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;u&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;JOIN&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;actions&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;ON&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;u&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;WHERE&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;u&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;email&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;alice@example.com&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&#xA;&lt;/div&gt;&#xA;&lt;/div&gt;&lt;p&gt;Syntactically clean. Ran without error. Returned zero rows — which the assistant reported back as &amp;ldquo;this user has no actions.&amp;rdquo; The real answer was that &lt;code&gt;users.id&lt;/code&gt; is a &lt;code&gt;BIGINT&lt;/code&gt; and &lt;code&gt;actions.id&lt;/code&gt; is a &lt;code&gt;CHAR(36)&lt;/code&gt; UUID. MySQL coerced the integer to a string, compared it to a UUID, and found no match. The join wasn&amp;rsquo;t wrong, exactly — it was meaningless, and the database had no way to say so.&lt;/p&gt;&#xA;&lt;p&gt;The experienced reader&amp;rsquo;s first fix is &amp;ldquo;just use UUIDs everywhere&amp;rdquo; or &amp;ldquo;enforce the type at join time.&amp;rdquo; Neither works. The footgun isn&amp;rsquo;t the type mismatch; it&amp;rsquo;s the column name. When every table&amp;rsquo;s primary key is named &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;a.id = b.id&lt;/code&gt; is a valid expression between any two tables in the schema, and nothing in the column names tells you whether that expression means anything. Fix the types and you close one failure mode; the identically-typed, semantically-unrelated &lt;code&gt;users.id = 42 = orders.id&lt;/code&gt; case still ships.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-nobody-can-see&#34;&gt;What nobody can see&#xA;&lt;/h2&gt;&lt;p&gt;The &lt;code&gt;&amp;lt;table&amp;gt;_id&lt;/code&gt; convention is older than most of us, and the case for it is usually framed as clarity or style. The sharper framing is that bare &lt;code&gt;id&lt;/code&gt; hides the information that matters most at the point of the join — which table&amp;rsquo;s identity is being compared, and whether comparing them makes sense — from every reader of the query.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The query&amp;rsquo;s reviewer.&lt;/strong&gt; &lt;code&gt;ON u.id = a.id&lt;/code&gt; gives no hint of what&amp;rsquo;s being matched. A human reviewer has to carry the table-to-alias mapping (&lt;code&gt;u&lt;/code&gt; is &lt;code&gt;users&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt; is &lt;code&gt;actions&lt;/code&gt;) and the table-to-type mapping (&lt;code&gt;users.id&lt;/code&gt; is BIGINT, &lt;code&gt;actions.id&lt;/code&gt; is UUID) in working memory, then cross-check them against the join condition. None of those steps are hard, but reviewers skip them because the column names look symmetric. Two &lt;code&gt;.id&lt;/code&gt; references read as &amp;ldquo;joining on primary keys,&amp;rdquo; which is the kind of join nobody flags.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The LLM reading the schema.&lt;/strong&gt; An assistant generating SQL from the catalog sees &lt;code&gt;users(id BIGINT, ...)&lt;/code&gt; and &lt;code&gt;actions(id CHAR(36), ...)&lt;/code&gt; as two tables with primary keys named &lt;code&gt;id&lt;/code&gt;. Absent a full column-type check on every candidate join (and most schema-reading prompts don&amp;rsquo;t do this), the natural-looking join between &amp;ldquo;a user and their actions&amp;rdquo; is &lt;code&gt;u.id = a.id&lt;/code&gt; — which is exactly wrong. The schema presented the column as joinable; the LLM took it at face value. The same mistake a tired human makes, but at scale and without fatigue to blame.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;The static analyzer.&lt;/strong&gt; Linters and schema-aware query builders operate on names first and types second. A rule that warns on suspicious cross-table joins has no signal to fire on when both sides are &lt;code&gt;.id&lt;/code&gt; — the column names match, so the join is &amp;ldquo;legitimate&amp;rdquo; by shape. The same rule on &lt;code&gt;users.user_id = actions.action_id&lt;/code&gt; would flag it immediately, because the names would be obviously non-corresponding.&lt;/p&gt;&#xA;&lt;p&gt;None of these readers are missing a step they should have taken. They&amp;rsquo;re all doing the reasonable thing, and the reasonable thing produces wrong queries because the schema is telling them &lt;code&gt;id&lt;/code&gt; is &lt;code&gt;id&lt;/code&gt; in both tables.&lt;/p&gt;&#xA;&lt;h2 id=&#34;three-failure-modes-ranked-by-how-loudly-they-fail&#34;&gt;Three failure modes, ranked by how loudly they fail&#xA;&lt;/h2&gt;&lt;p&gt;Three distinct outcomes hide behind &lt;code&gt;a.id = b.id&lt;/code&gt;, and they don&amp;rsquo;t fail equally:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;PostgreSQL, mixed types.&lt;/strong&gt; The comparison errors out with &lt;code&gt;operator does not exist: bigint = uuid&lt;/code&gt;. Loud, caught in development, fixed before merge. The best failure mode.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;MySQL, mixed types.&lt;/strong&gt; Silent coercion to string, zero rows returned. The opening example. Bad, because &amp;ldquo;no results&amp;rdquo; looks like valid data to every downstream consumer.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Either engine, same type but semantically unrelated.&lt;/strong&gt; &lt;code&gt;BIGINT users.id = 42&lt;/code&gt; matched against &lt;code&gt;BIGINT orders.id = 42&lt;/code&gt; returns the rows where the integers happen to collide. The query runs, the result set isn&amp;rsquo;t empty, and the rows look plausible because they&amp;rsquo;re real rows from real tables. The worst failure mode, because nothing about the output signals that the join was nonsense.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The first two are loud enough to catch in review. The third is the one that ships. And the third is the default once more than one table in the schema uses a plain &lt;code&gt;BIGINT&lt;/code&gt; &lt;code&gt;id&lt;/code&gt; — which is almost every relational schema in existence.&lt;/p&gt;&#xA;&lt;div class=&#34;warning-box&#34;&gt;&#xA;    &lt;strong&gt;Zero rows looks like no data&lt;/strong&gt;&#xA;    &lt;div&gt;A join that silently returns zero rows because of a type coercion is indistinguishable from a join that legitimately has no matches. Code generators, dashboards, and AI assistants all interpret empty results as &amp;ldquo;the relationship exists but has no rows,&amp;rdquo; not &amp;ldquo;the query is nonsense.&amp;rdquo; The failure hides inside success.&lt;/div&gt;&#xA;&lt;/div&gt;&#xA;&#xA;&lt;h2 id=&#34;mixed-pk-types-make-the-naming-problem-sharper&#34;&gt;Mixed PK types make the naming problem sharper&#xA;&lt;/h2&gt;&lt;p&gt;Production schemas rarely stay on one PK strategy for long. The original tables are usually &lt;code&gt;BIGINT AUTO_INCREMENT&lt;/code&gt; because the framework defaulted to it; a newer service switches to UUIDs to let clients generate IDs offline or to distribute across shards; join tables pick up composite keys because &lt;code&gt;(user_id, role_id)&lt;/code&gt; is the natural identity. Nothing in the schema announces which tables fall into which bucket — &lt;code&gt;SHOW CREATE TABLE&lt;/code&gt; or &lt;code&gt;\d&lt;/code&gt; is the only source of truth, and even that requires reading every table to know what joins are legal.&lt;/p&gt;&#xA;&lt;p&gt;Mixed types are where the naming footgun turns from theoretical to frequent. When every PK was a BIGINT, the &amp;ldquo;same type but semantically unrelated&amp;rdquo; case was the main risk and reviewers caught most of it. Once the schema has BIGINT and UUID sitting next to each other — all named &lt;code&gt;id&lt;/code&gt; — the mismatched-type cases pile on top, and &amp;ldquo;no data found&amp;rdquo; becomes a regular report from any tool generating queries from the schema.&lt;/p&gt;&#xA;&lt;p&gt;The sizing question — when to pick BIGINT versus UUID versus UUIDv7 versus composite, and what each costs at the index level — is covered separately in &lt;a class=&#34;link&#34; href=&#34;https://explainanalyze.com/p/random-uuids-as-primary-keys-the-b-tree-penalty/&#34; &gt;Random UUIDs as Primary Keys&lt;/a&gt;. The two problems interact but have independent fixes: pick your PK types deliberately, &lt;em&gt;and&lt;/em&gt; name them so the schema describes its own relationships. Neither fix substitutes for the other.&lt;/p&gt;&#xA;&lt;h2 id=&#34;naming-is-the-lever-that-actually-helps&#34;&gt;Naming is the lever that actually helps&#xA;&lt;/h2&gt;&lt;p&gt;Naming is what makes a schema describe its own relationships without requiring the reader — human or otherwise — to open every &lt;code&gt;CREATE TABLE&lt;/code&gt;. Two conventions, consistently applied, close most of the gap:&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Name the primary key after the table.&lt;/strong&gt; &lt;code&gt;users.user_id&lt;/code&gt;, &lt;code&gt;orders.order_id&lt;/code&gt;, &lt;code&gt;actions.action_id&lt;/code&gt;. The equality &lt;code&gt;users.user_id = orders.order_id&lt;/code&gt; reads as obvious nonsense, because the column names are no longer identical. Reviewers see it, LLMs don&amp;rsquo;t produce it, linters can flag it. The cost is a small amount of redundancy in queries (&lt;code&gt;users.user_id&lt;/code&gt; instead of &lt;code&gt;users.id&lt;/code&gt;), which is almost always a fair trade. This lines up with the broader guidance in &lt;a class=&#34;link&#34; href=&#34;https://explainanalyze.com/p/schema-conventions-dont-survive-without-automation/&#34; &gt;Schema Conventions and Why They Matter&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Foreign keys mirror the target PK.&lt;/strong&gt; &lt;code&gt;orders.user_id&lt;/code&gt; clearly references &lt;code&gt;users.user_id&lt;/code&gt;. &lt;code&gt;actions.user_id&lt;/code&gt; clearly references &lt;code&gt;users.user_id&lt;/code&gt;. This is already common practice; the only change is that the target&amp;rsquo;s PK name matches, closing the loop. &lt;a class=&#34;link&#34; href=&#34;https://explainanalyze.com/p/foreign-keys-are-not-optional/&#34; &gt;Foreign Keys Are Not Optional&lt;/a&gt; covers why the FK itself matters; naming is what makes the FK legible without the &lt;code&gt;REFERENCES&lt;/code&gt; clause in hand.&lt;/p&gt;&#xA;&lt;p&gt;The bare &lt;code&gt;id&lt;/code&gt; convention is defensible when the PK column only ever shows up in queries alongside its table name (&lt;code&gt;users.id&lt;/code&gt;) and never as a bare &lt;code&gt;id&lt;/code&gt; in a SELECT list or join condition. That discipline is hard to enforce across a team over years, and every framework&amp;rsquo;s default query builder produces &lt;code&gt;SELECT id FROM users&lt;/code&gt; without thinking about it. The naming fix makes the discipline unnecessary.&lt;/p&gt;&#xA;&lt;h2 id=&#34;when-bare-id-is-actually-fine&#34;&gt;When bare &lt;code&gt;id&lt;/code&gt; is actually fine&#xA;&lt;/h2&gt;&lt;p&gt;Not every schema needs to bend. A small application, a service with a handful of tables, or a database where every query is reviewed by one team has plenty of context to keep the &lt;code&gt;a.id = b.id&lt;/code&gt; landmine out of reach. The cost of the convention scales with the number of tables, the number of engineers, and the number of non-human query generators; in the small case it rarely shows up.&lt;/p&gt;&#xA;&lt;p&gt;What changes once any of those numbers grow: nobody remembers which tables are BIGINT versus UUID, the assistant pattern of generating queries from schema is routine, and the review process that caught &lt;code&gt;a.id = b.id&lt;/code&gt; in a 20-table schema can&amp;rsquo;t read every join in a 400-table one. At that size the convention pays rent, and renaming PKs is a migration that gets slower every quarter.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-bigger-picture&#34;&gt;The bigger picture&#xA;&lt;/h2&gt;&lt;p&gt;A schema&amp;rsquo;s job isn&amp;rsquo;t just to hold data correctly; it&amp;rsquo;s to describe its own shape well enough that the tools reading it can reason about relationships without reading every line. The bare &lt;code&gt;id&lt;/code&gt; PK is a small departure from that — one column name shared across tables — but it&amp;rsquo;s the departure that most consistently produces silent-wrong-answer queries, because SQL has no way to distinguish &amp;ldquo;same name, same meaning&amp;rdquo; from &amp;ldquo;same name, different meaning.&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;Name the primary key after the table it identifies, so the schema tells its own story when someone — human or otherwise — joins two of them together. It costs almost nothing on day one and leaves the schema legible at 400 tables, which is where most of us end up.&lt;/p&gt;&#xA;</description>
        </item></channel>
</rss>
