<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Database on Kinoko's TIL Log</title><link>https://kinoko-tech-blog-theta.vercel.app/tags/database/</link><description>Recent content in Database on Kinoko's TIL Log</description><generator>Hugo -- gohugo.io</generator><language>en</language><managingEditor>pippimotta@gmail.com (Kinoko)</managingEditor><webMaster>pippimotta@gmail.com (Kinoko)</webMaster><copyright>&amp;copy; Brewed by Kinoko</copyright><lastBuildDate>Mon, 23 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://kinoko-tech-blog-theta.vercel.app/tags/database/index.xml" rel="self" type="application/rss+xml"/><item><title>dbtpl Code Generation</title><link>https://kinoko-tech-blog-theta.vercel.app/posts/dbtpl-code-generation/</link><pubDate>Mon, 23 Mar 2026 00:00:00 +0000</pubDate><author>pippimotta@gmail.com (Kinoko)</author><guid>https://kinoko-tech-blog-theta.vercel.app/posts/dbtpl-code-generation/</guid><description>&lt;h2 id="the-point"&gt;The Point&lt;/h2&gt;
&lt;p&gt;dbtpl (part of the xo ecosystem) connects to a database, introspects the schema, and generates type-safe structs and query methods through Go templates. Hand-written DB structs easily drift from the schema; dbtpl makes the schema the single source of truth.&lt;/p&gt;
&lt;h2 id="explanation"&gt;Explanation&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;How it works&lt;/strong&gt;&lt;/p&gt;





&lt;pre tabindex="0"&gt;&lt;code&gt;Database (PostgreSQL / MySQL / SQLite...)
 ↓ introspect schema (tables, columns, types, indexes, FK...)
dbtpl
 ↓ apply Go templates
Generated Go code (structs, query methods)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;No need to maintain DB structs by hand &amp;ndash; when the schema changes, re-run dbtpl to sync.&lt;/p&gt;</description><content:encoded><![CDATA[<h2 id="the-point">The Point</h2>
<p>dbtpl (part of the xo ecosystem) connects to a database, introspects the schema, and generates type-safe structs and query methods through Go templates. Hand-written DB structs easily drift from the schema; dbtpl makes the schema the single source of truth.</p>
<h2 id="explanation">Explanation</h2>
<p><strong>How it works</strong></p>





<pre tabindex="0"><code>Database (PostgreSQL / MySQL / SQLite...)
    ↓ introspect schema (tables, columns, types, indexes, FK...)
dbtpl
    ↓ apply Go templates
Generated Go code (structs, query methods)</code></pre><p>No need to maintain DB structs by hand &ndash; when the schema changes, re-run dbtpl to sync.</p>
<p><strong>Two modes</strong></p>
<p><strong>Schema mode</strong>: generate from the entire DB schema</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">dbtpl schema postgres://user:pass@host/dbname -o ./models</span></span></code></pre></div><p>Generates a Go struct for each table, including methods related to primary keys, foreign keys, and indexes.</p>
<p><strong>Query mode</strong>: generate type-safe result structs from custom SQL</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">dbtpl query postgres://user:pass@host/dbname <span class="s">&lt;&lt; ENDSQL
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="s">SELECT a.name::varchar AS name, b.type::integer AS my_type
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="s">FROM authors a
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="s">JOIN authortypes b ON a.id = b.author_id
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="s">WHERE a.id = %%authorID int%%
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="s">ENDSQL</span></span></span></code></pre></div><p><code>%%param type%%</code> is dbtpl&rsquo;s query parameter syntax, which generates a corresponding function signature.</p>
<p><strong>What gets generated</strong></p>
<ul>
<li>Go structs matching each table (field types aligned with DB schema)</li>
<li>CRUD query methods (Insert, Update, Delete, Get by PK)</li>
<li>Enum types</li>
<li>Struct tags (<code>db:&quot;column_name&quot;</code> etc.)</li>
</ul>
<p><strong>Template customization</strong></p>
<p>dbtpl&rsquo;s generation logic uses Go <code>text/template</code>. You can dump the built-in templates and modify them:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">dbtpl dump --src base -o ./custom-templates
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># edit the .tpl files in custom-templates/</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">dbtpl schema postgres://... --src ./custom-templates</span></span></code></pre></div><h2 id="knowledge-sugar">Knowledge Sugar</h2>
<p><strong>Why not just hand-write structs?</strong></p>
<table>
	<thead>
			<tr>
					<th></th>
					<th>Hand-written structs</th>
					<th>dbtpl generated</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Schema sync</td>
					<td>Manual maintenance, easy to drift</td>
					<td>Re-run to sync</td>
			</tr>
			<tr>
					<td>Type safety</td>
					<td>Relies on the engineer to verify</td>
					<td>Derived from DB schema</td>
			</tr>
			<tr>
					<td>After migration</td>
					<td>Must remember to update structs</td>
					<td>Just re-run</td>
			</tr>
	</tbody>
</table>
<p><strong>dbtpl vs sqlc</strong></p>
<p>Both generate Go code from a database, but they differ in approach:</p>
<ul>
<li><strong>sqlc</strong>: SQL-query-centric &ndash; write SQL first, then generate the corresponding functions</li>
<li><strong>dbtpl</strong>: schema-centric &ndash; introspects the entire DB, with more template flexibility</li>
</ul>
]]></content:encoded></item><item><title>MySQL Foreign Key vs JOIN</title><link>https://kinoko-tech-blog-theta.vercel.app/posts/mysql-foreign-key-vs-join/</link><pubDate>Wed, 11 Mar 2026 00:00:00 +0000</pubDate><author>pippimotta@gmail.com (Kinoko)</author><guid>https://kinoko-tech-blog-theta.vercel.app/posts/mysql-foreign-key-vs-join/</guid><description>&lt;h2 id="the-point"&gt;The Point&lt;/h2&gt;
&lt;p&gt;Foreign keys and JOINs solve two different problems: FK ensures data integrity at the &lt;strong&gt;write&lt;/strong&gt; layer, while JOIN associates data at the &lt;strong&gt;read&lt;/strong&gt; layer. Not having a FK does not prevent JOINs, but you lose the database-level safety net.&lt;/p&gt;
&lt;h2 id="explanation"&gt;Explanation&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Two core functions of a Foreign Key&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;shipping_fees&lt;/code&gt; (FK) -&amp;gt; &lt;code&gt;shipping_classes&lt;/code&gt; (PK) as an example:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Referential integrity on insert&lt;/strong&gt;: &lt;code&gt;shipping_fees&lt;/code&gt; cannot contain a &lt;code&gt;shipping_class_id&lt;/code&gt; that does not exist in &lt;code&gt;shipping_classes&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cascading protection on delete&lt;/strong&gt;: as long as any &lt;code&gt;shipping_fees&lt;/code&gt; row references a &lt;code&gt;shipping_class&lt;/code&gt;, that &lt;code&gt;shipping_class&lt;/code&gt; row cannot be deleted&lt;/li&gt;
&lt;/ol&gt;





&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;1&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- With FK, the database blocks both of these:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;2&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INTO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shipping_fees&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shipping_class_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;VALUES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;3&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- ERROR: Cannot add or update a child row: foreign key constraint fails
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;4&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;5&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shipping_classes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;6&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- ERROR: Cannot delete or update a parent row: foreign key constraint fails&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;JOIN does something different&lt;/strong&gt;&lt;/p&gt;</description><content:encoded><![CDATA[<h2 id="the-point">The Point</h2>
<p>Foreign keys and JOINs solve two different problems: FK ensures data integrity at the <strong>write</strong> layer, while JOIN associates data at the <strong>read</strong> layer. Not having a FK does not prevent JOINs, but you lose the database-level safety net.</p>
<h2 id="explanation">Explanation</h2>
<p><strong>Two core functions of a Foreign Key</strong></p>
<p>Using <code>shipping_fees</code> (FK) -&gt; <code>shipping_classes</code> (PK) as an example:</p>
<ol>
<li><strong>Referential integrity on insert</strong>: <code>shipping_fees</code> cannot contain a <code>shipping_class_id</code> that does not exist in <code>shipping_classes</code></li>
<li><strong>Cascading protection on delete</strong>: as long as any <code>shipping_fees</code> row references a <code>shipping_class</code>, that <code>shipping_class</code> row cannot be deleted</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- With FK, the database blocks both of these:
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">shipping_fees</span><span class="w"> </span><span class="p">(</span><span class="n">shipping_class_id</span><span class="p">,</span><span class="w"> </span><span class="p">...)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">999</span><span class="p">,</span><span class="w"> </span><span class="p">...);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1">-- ERROR: Cannot add or update a child row: foreign key constraint fails
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">DELETE</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">shipping_classes</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">-- ERROR: Cannot delete or update a parent row: foreign key constraint fails</span></span></span></code></pre></div><p><strong>JOIN does something different</strong></p>
<p>JOIN only associates data from two tables at query time. It does not care whether the data is valid:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="n">sf</span><span class="p">.</span><span class="o">*</span><span class="p">,</span><span class="w"> </span><span class="n">sc</span><span class="p">.</span><span class="n">name</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="n">shipping_fees</span><span class="w"> </span><span class="n">sf</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">JOIN</span><span class="w"> </span><span class="n">shipping_classes</span><span class="w"> </span><span class="n">sc</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">sf</span><span class="p">.</span><span class="n">shipping_class_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sc</span><span class="p">.</span><span class="n">id</span><span class="p">;</span></span></span></code></pre></div><p>You can JOIN without a FK, but if there are orphan records, the JOIN silently excludes them without raising an error.</p>
<p><strong>Why do DBRE teams dislike FK?</strong></p>
<p>FK causes trouble at the DB operations layer:</p>
<ul>
<li>Emergency data fixes via DML (direct INSERT / DELETE) can be blocked by FK constraints</li>
<li>Large data migrations require temporarily disabling FK checks</li>
<li>FK behavior gets more complex in replication setups</li>
</ul>
<p><strong>The compromise</strong></p>
<p>Keep the JOIN design (no FK), but the DBRE team uses <strong>DML validation</strong> at the operations layer to ensure data correctness &ndash; essentially replacing FK&rsquo;s two protections with a manual process.</p>
<h2 id="knowledge-sugar">Knowledge Sugar</h2>
<p><strong>FK vs Application-level validation vs DML validation</strong></p>
<table>
	<thead>
			<tr>
					<th>Layer</th>
					<th>Approach</th>
					<th>Pros</th>
					<th>Cons</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>DB (FK)</td>
					<td>Foreign Key constraint</td>
					<td>Absolute protection, blocks all entry points</td>
					<td>Low operational flexibility, can block during emergencies</td>
			</tr>
			<tr>
					<td>Application</td>
					<td>Validate in code</td>
					<td>High flexibility</td>
					<td>Only protects traffic through the app; bypassing the app means no protection</td>
			</tr>
			<tr>
					<td>DML validation</td>
					<td>Operational process rules</td>
					<td>Best of both worlds</td>
					<td>Relies on manual process, risk of human error</td>
			</tr>
	</tbody>
</table>
<p><strong>The nature of this tradeoff</strong></p>
<p>FK means &ldquo;let the database worry about data correctness.&rdquo; No FK means &ldquo;we worry about it ourselves.&rdquo; The former is safer but sacrifices operational flexibility; the latter is more flexible but shifts responsibility to the application and operations layers.</p>
<p>For high-traffic systems or those with urgent on-call needs, DBRE teams tend to prefer the latter &ndash; they do not want to be stuck on a constraint during a critical moment.</p>
]]></content:encoded></item><item><title>MySQL Lock</title><link>https://kinoko-tech-blog-theta.vercel.app/posts/mysql-lock/</link><pubDate>Thu, 26 Feb 2026 00:00:00 +0000</pubDate><author>pippimotta@gmail.com (Kinoko)</author><guid>https://kinoko-tech-blog-theta.vercel.app/posts/mysql-lock/</guid><description>&lt;h2 id="the-point"&gt;The Point&lt;/h2&gt;
&lt;p&gt;When a MySQL lock is not properly released, other transactions trying to access the same data keep waiting indefinitely, causing batch jobs to get stuck (blocking).&lt;/p&gt;
&lt;h2 id="explanation"&gt;Explanation&lt;/h2&gt;
&lt;p&gt;MySQL uses &lt;strong&gt;locks&lt;/strong&gt; to protect data consistency &amp;ndash; when a transaction is reading or writing a row, it acquires a lock on it first, forcing other transactions to queue up and wait.&lt;/p&gt;
&lt;p&gt;Under normal circumstances, locks are automatically released after a transaction &lt;code&gt;COMMIT&lt;/code&gt;s or &lt;code&gt;ROLLBACK&lt;/code&gt;s. But if a transaction gets stuck for some reason (e.g., a connection not properly closed, a process crash, or a long-running query), the lock persists and everything behind it piles up.&lt;/p&gt;</description><content:encoded><![CDATA[<h2 id="the-point">The Point</h2>
<p>When a MySQL lock is not properly released, other transactions trying to access the same data keep waiting indefinitely, causing batch jobs to get stuck (blocking).</p>
<h2 id="explanation">Explanation</h2>
<p>MySQL uses <strong>locks</strong> to protect data consistency &ndash; when a transaction is reading or writing a row, it acquires a lock on it first, forcing other transactions to queue up and wait.</p>
<p>Under normal circumstances, locks are automatically released after a transaction <code>COMMIT</code>s or <code>ROLLBACK</code>s. But if a transaction gets stuck for some reason (e.g., a connection not properly closed, a process crash, or a long-running query), the lock persists and everything behind it piles up.</p>
<p>In this case, a transaction was holding a lock without finishing. The batch job kept waiting. The DBRE team manually terminated that session with a <code>KILL</code> command, which released the lock and allowed the batch job to resume.</p>
<h2 id="knowledge-sugar">Knowledge Sugar</h2>
<p><strong>Common lock types:</strong></p>
<ul>
<li><strong>Row lock</strong>: locks only the specific rows; fine-grained; InnoDB&rsquo;s default</li>
<li><strong>Table lock</strong>: locks the entire table; coarse-grained; used by MyISAM</li>
<li><strong>Gap lock</strong>: locks the &ldquo;gaps&rdquo; between index ranges to prevent phantom reads</li>
</ul>
<p><strong>How to find out who is blocking whom:</strong></p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- Check which locks are currently waiting
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">information_schema</span><span class="p">.</span><span class="n">INNODB_LOCK_WAITS</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">-- View all active transactions
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">information_schema</span><span class="p">.</span><span class="n">INNODB_TRX</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1">-- Manually terminate a stuck session (what the DBRE team did)
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="n">KILL</span><span class="w"> </span><span class="o">&lt;</span><span class="n">process_id</span><span class="o">&gt;</span><span class="p">;</span></span></span></code></pre></div><p><strong>Why are batch jobs particularly prone to this?</strong>
Batch jobs typically process large volumes of data with long-running transactions. If another transaction also needs the same set of rows, deadlocks or long blocking are more likely to occur.</p>
<p><strong>Related concepts to explore further:</strong></p>
<ul>
<li>Deadlock vs Blocking: deadlock is mutual waiting (both sides stuck); blocking is one-directional waiting</li>
<li>InnoDB&rsquo;s <code>innodb_lock_wait_timeout</code> setting (auto-abort on timeout)</li>
<li><code>SELECT ... FOR UPDATE</code> explicitly acquires a row lock</li>
</ul>
]]></content:encoded></item></channel></rss>