<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Database on</title><link>/tags/database/</link><description>Recent content in Database on</description><generator>Hugo</generator><language>en</language><lastBuildDate>Wed, 18 Feb 2026 00:00:00 +0000</lastBuildDate><atom:link href="/tags/database/index.xml" rel="self" type="application/rss+xml"/><item><title>Lesson 9: Testing with Real Databases — Mocking sql.DB is lying to yourself</title><link>/post/go/go-db-testing/</link><pubDate>Wed, 18 Feb 2026 00:00:00 +0000</pubDate><guid>/post/go/go-db-testing/</guid><description>&lt;p&gt;For years, the standard advice for testing Go database code was to use &lt;code&gt;sqlmock&lt;/code&gt; — a library that intercepts database calls and lets you assert which queries were run. I used it for a while. Then I started finding production bugs that my &lt;code&gt;sqlmock&lt;/code&gt; tests were actively hiding: constraint violations that only happen with real Postgres, query plan differences, JSON operator behavior, NULL handling edge cases, transaction isolation behavior. &lt;code&gt;sqlmock&lt;/code&gt; tests were passing while the same code was failing in production. That&amp;rsquo;s worse than no tests at all.&lt;/p&gt;</description></item><item><title>Lesson 8: Migrations Without Downtime — ALTER TABLE can lock your entire database</title><link>/post/go/go-db-migrations/</link><pubDate>Tue, 27 Jan 2026 00:00:00 +0000</pubDate><guid>/post/go/go-db-migrations/</guid><description>&lt;p&gt;The first time I caused a production outage with a database migration, I was adding a &lt;code&gt;NOT NULL&lt;/code&gt; column to a table. The migration looked innocent. It ran fine on staging with 1,000 rows. In production with 40 million rows, it locked the entire table for 11 minutes while Postgres rewrote every row. Every write to that table failed with a lock timeout. We rolled back the app but couldn&amp;rsquo;t roll back the migration. It was a terrible morning.&lt;/p&gt;</description></item><item><title>Lesson 7: The N+1 Problem — One query per row is a performance bug</title><link>/post/go/go-db-n-plus-one/</link><pubDate>Thu, 25 Dec 2025 00:00:00 +0000</pubDate><guid>/post/go/go-db-n-plus-one/</guid><description>&lt;p&gt;The N+1 problem is the most common database performance issue I find when reviewing Go code, and it&amp;rsquo;s especially sneaky because it looks totally fine in development. You have a list of 10 users, you fetch their orders, 11 queries, no problem. You deploy to production, the table has 50,000 users, your endpoint suddenly takes 40 seconds, and you get a 3am page. The queries were always there — you just didn&amp;rsquo;t notice them until the data grew.&lt;/p&gt;</description></item><item><title>Lesson 6: Context with Database Queries — Every query needs a timeout</title><link>/post/go/go-db-context-queries/</link><pubDate>Wed, 03 Dec 2025 00:00:00 +0000</pubDate><guid>/post/go/go-db-context-queries/</guid><description>&lt;p&gt;We once had a query that ran for 47 minutes. I&amp;rsquo;m not joking. A reporting query that worked fine on the test dataset decided to do a full sequential scan on a 200M-row table in production because someone dropped an index by accident the night before. The query just ran. And ran. And ran. The connection was held the whole time, blocking the pool. New requests started queuing. Within 10 minutes, the service was effectively down — not because of an error, but because every database connection was held by queries waiting for that one slow one to finish.&lt;/p&gt;</description></item><item><title>Lesson 5: sqlc vs ORM vs Raw SQL — Pick your tradeoff, not your religion</title><link>/post/go/go-db-sqlc-vs-orm/</link><pubDate>Wed, 29 Oct 2025 00:00:00 +0000</pubDate><guid>/post/go/go-db-sqlc-vs-orm/</guid><description>&lt;p&gt;Every time someone asks &amp;ldquo;should I use GORM or raw SQL?&amp;rdquo; a flame war breaks out. I&amp;rsquo;ve been on both sides of that argument, and I&amp;rsquo;ve shipped production systems using all four approaches — raw SQL, GORM, sqlc, and Ent. My opinion now is boring: each one is the right choice in a specific context, and none of them is universally correct. The question isn&amp;rsquo;t which one is best, it&amp;rsquo;s which tradeoffs you&amp;rsquo;re signing up for.&lt;/p&gt;</description></item><item><title>Lesson 4: The Repository Pattern — Sometimes a function is enough</title><link>/post/go/go-db-repository-pattern/</link><pubDate>Thu, 02 Oct 2025 00:00:00 +0000</pubDate><guid>/post/go/go-db-repository-pattern/</guid><description>&lt;p&gt;The repository pattern is one of those ideas that sounds great in an architecture talk and causes real pain when applied indiscriminately to a Go codebase. I&amp;rsquo;ve seen teams add a &lt;code&gt;UserRepository&lt;/code&gt; interface with five methods, a concrete &lt;code&gt;postgresUserRepository&lt;/code&gt; implementation, and a &lt;code&gt;mockUserRepository&lt;/code&gt; for tests — and then wonder why everything takes three times as long to write. I&amp;rsquo;ve also seen teams skip the pattern entirely and end up with &lt;code&gt;*sql.DB&lt;/code&gt; threaded through 40 different functions, impossible to test without a real database.&lt;/p&gt;</description></item><item><title>Lesson 3: Transactions That Don't Bite — Begin, defer rollback, commit</title><link>/post/go/go-db-transactions/</link><pubDate>Sun, 07 Sep 2025 00:00:00 +0000</pubDate><guid>/post/go/go-db-transactions/</guid><description>&lt;p&gt;Transactions are the part of database programming where &amp;ldquo;it&amp;rsquo;s fine most of the time&amp;rdquo; really isn&amp;rsquo;t good enough. A buggy SELECT just returns wrong data. A buggy transaction can leave your database in a half-written state — an order placed without inventory decremented, money debited without the transfer completing, a user created without their profile record. The bugs are subtle, often don&amp;rsquo;t manifest in testing, and only show up in production when two things happen at the same time.&lt;/p&gt;</description></item><item><title>Lesson 2: Connection Pool Tuning — Your pool config is probably wrong</title><link>/post/go/go-db-connection-pools/</link><pubDate>Fri, 08 Aug 2025 00:00:00 +0000</pubDate><guid>/post/go/go-db-connection-pools/</guid><description>&lt;p&gt;Here&amp;rsquo;s a scenario that played out for me once: service is running fine for weeks, then we double traffic, and suddenly requests start timing out. Not all of them — maybe 10%. The logs show &lt;code&gt;pq: sorry, too many clients already&lt;/code&gt;. Postgres is refusing connections. But we have a connection pool — that&amp;rsquo;s the whole point, right? Why is Postgres seeing more connections than it can handle?&lt;/p&gt;
&lt;p&gt;Because &lt;code&gt;database/sql&lt;/code&gt;&amp;rsquo;s default pool settings are almost certainly wrong for production, and most people never change them.&lt;/p&gt;</description></item><item><title>Lesson 1: database/sql Done Right — The stdlib is better than you think</title><link>/post/go/go-db-sql-done-right/</link><pubDate>Tue, 08 Jul 2025 00:00:00 +0000</pubDate><guid>/post/go/go-db-sql-done-right/</guid><description>&lt;p&gt;I spent a long time reaching for third-party database libraries in Go before I actually read the &lt;code&gt;database/sql&lt;/code&gt; docs. When I finally did, I was embarrassed — the stdlib had everything I needed and I&amp;rsquo;d been adding dependencies for no reason. The problem isn&amp;rsquo;t that &lt;code&gt;database/sql&lt;/code&gt; is limited. The problem is that it has a handful of non-obvious behaviors that, if you don&amp;rsquo;t know about them, will burn you in production. Once you internalize those, you&amp;rsquo;ll write better database code than most people using ORMs.&lt;/p&gt;</description></item><item><title>Lesson 10: Redis, MongoDB, and Non-Relational Stores — Beyond SQL</title><link>/post/rust/rust-db-nosql/</link><pubDate>Sun, 10 Nov 2024 15:37:00 +0000</pubDate><guid>/post/rust/rust-db-nosql/</guid><description>&lt;p&gt;I spent a week optimizing a Postgres query that powered a leaderboard. It involved a complex window function over millions of rows, and no amount of indexing got it under 200ms. Then a senior engineer walked over, looked at the query, and said &amp;ldquo;Why isn&amp;rsquo;t this in Redis?&amp;rdquo; He was right. I replaced 30 lines of SQL with a sorted set and the response time dropped to 2ms.&lt;/p&gt;
&lt;p&gt;Not every data problem is a SQL problem. Sometimes you need the right tool, not a better query plan.&lt;/p&gt;</description></item><item><title>Lesson 9: N+1 Queries, Indexes, and EXPLAIN — Database performance in Rust</title><link>/post/rust/rust-db-performance/</link><pubDate>Wed, 06 Nov 2024 07:52:00 +0000</pubDate><guid>/post/rust/rust-db-performance/</guid><description>&lt;p&gt;A coworker asked me to look at an endpoint that was taking 8 seconds to return 50 orders. The table had 200K rows — not big by any standard. I opened the code, and the pattern was immediately obvious: fetch 50 orders, then for each order, fetch its items in a separate query. Fifty-one database round trips where one would do.&lt;/p&gt;
&lt;p&gt;The N+1 query problem is the most common performance mistake in database-backed applications, and Rust doesn&amp;rsquo;t magically prevent it. You need to know what to look for.&lt;/p&gt;</description></item><item><title>Lesson 8: Testing with Real Databases — No more mocking SQL</title><link>/post/rust/rust-db-testing/</link><pubDate>Sun, 03 Nov 2024 13:15:00 +0000</pubDate><guid>/post/rust/rust-db-testing/</guid><description>&lt;p&gt;I spent two days debugging a production issue where a query returned duplicate rows. The unit tests all passed — every mock returned exactly the expected data. The problem was a missing &lt;code&gt;DISTINCT&lt;/code&gt; in a JOIN query that only manifested with real data containing multiple matching rows. The mocks were too perfect. They never produced the messy data that real databases contain.&lt;/p&gt;
&lt;p&gt;That was when I stopped mocking SQL.&lt;/p&gt;
&lt;h2 id="the-problem-with-mocking-database-calls"&gt;The Problem with Mocking Database Calls&lt;/h2&gt;
&lt;p&gt;Mocking databases is popular because it&amp;rsquo;s convenient. You don&amp;rsquo;t need Docker, you don&amp;rsquo;t need a test database, your tests run in milliseconds. But you&amp;rsquo;re testing the wrong thing.&lt;/p&gt;</description></item><item><title>Lesson 7: Building Type-Safe Query Builders — Queries that can't be wrong</title><link>/post/rust/rust-db-query-builder/</link><pubDate>Fri, 01 Nov 2024 09:28:00 +0000</pubDate><guid>/post/rust/rust-db-query-builder/</guid><description>&lt;p&gt;I was reviewing a PR that had a search endpoint with 12 optional filters. The handler was a 200-line function full of &lt;code&gt;if let Some(...)&lt;/code&gt; blocks, each appending a different SQL fragment to a &lt;code&gt;String&lt;/code&gt;. It worked — until someone forgot a space between &lt;code&gt;AND&lt;/code&gt; and a column name, and the query silently returned zero results instead of erroring. No compile error. No test failure. Just a missing space in a string.&lt;/p&gt;</description></item><item><title>Lesson 6: The Repository Pattern in Rust — Abstracting persistence</title><link>/post/rust/rust-db-repository-pattern/</link><pubDate>Wed, 30 Oct 2024 16:40:00 +0000</pubDate><guid>/post/rust/rust-db-repository-pattern/</guid><description>&lt;p&gt;I once inherited a codebase where every HTTP handler had raw SQL queries inline — &lt;code&gt;sqlx::query!&lt;/code&gt; calls scattered through 80+ route handlers. Changing a table name meant grep-and-replace across the entire project. Adding a cache layer meant touching every handler. Testing a handler meant spinning up a real database. It worked, technically, but nobody wanted to touch it.&lt;/p&gt;
&lt;p&gt;The repository pattern fixes this. It puts a wall between your business logic and your database, and that wall pays for itself fast.&lt;/p&gt;</description></item><item><title>Lesson 5: Transactions and Error Rollback — Atomic operations</title><link>/post/rust/rust-db-transactions/</link><pubDate>Mon, 28 Oct 2024 10:05:00 +0000</pubDate><guid>/post/rust/rust-db-transactions/</guid><description>&lt;p&gt;A payment service I worked on had a subtle bug: it deducted money from the user&amp;rsquo;s wallet, then tried to create an order record. If the order insert failed — constraint violation, timeout, anything — the money was already gone. The user&amp;rsquo;s balance was decremented but they had no order. We called these &amp;ldquo;ghost charges&amp;rdquo; internally, and customers called them something less polite.&lt;/p&gt;
&lt;p&gt;The fix was embarrassingly simple: wrap both operations in a transaction.&lt;/p&gt;</description></item><item><title>Lesson 4: Schema Migrations in Rust Projects — Evolving your database</title><link>/post/rust/rust-db-migrations/</link><pubDate>Sat, 26 Oct 2024 19:12:00 +0000</pubDate><guid>/post/rust/rust-db-migrations/</guid><description>&lt;p&gt;A teammate once ran &lt;code&gt;ALTER TABLE orders DROP COLUMN status&lt;/code&gt; on the production database because he&amp;rsquo;d tested it locally and &amp;ldquo;it worked fine.&amp;rdquo; What he didn&amp;rsquo;t realize was that three other services depended on that column, and they all started throwing errors simultaneously. We spent the evening restoring from a backup.&lt;/p&gt;
&lt;p&gt;Schema migrations exist to prevent exactly this — they&amp;rsquo;re version control for your database.&lt;/p&gt;
&lt;h2 id="the-problem"&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Your database schema isn&amp;rsquo;t static. Features get added, requirements change, data models evolve. You need a way to:&lt;/p&gt;</description></item><item><title>Lesson 3: Connection Pooling with deadpool and bb8 — Managing database connections</title><link>/post/rust/rust-db-connection-pools/</link><pubDate>Thu, 24 Oct 2024 14:35:00 +0000</pubDate><guid>/post/rust/rust-db-connection-pools/</guid><description>&lt;p&gt;I once watched a Rust service fall over under modest load — maybe 200 concurrent requests — because every request opened a new Postgres connection, used it for one query, and dropped it. The database was spending more time on TLS handshakes and connection setup than on actual queries. CPU was fine, memory was fine, but &lt;code&gt;pg_stat_activity&lt;/code&gt; showed 200+ connections churning constantly. The fix took ten lines of code: add a connection pool.&lt;/p&gt;</description></item><item><title>Lesson 2: Diesel — The ORM approach</title><link>/post/rust/rust-db-diesel-intro/</link><pubDate>Tue, 22 Oct 2024 08:47:00 +0000</pubDate><guid>/post/rust/rust-db-diesel-intro/</guid><description>&lt;p&gt;I was two weeks into a project where I had to write about 40 CRUD endpoints for an admin panel. Each one needed the same pattern: validate input, build a query, map results to a struct, handle errors. By endpoint number six, I was copy-pasting SQLx queries and changing column names. That&amp;rsquo;s when a colleague asked, &amp;ldquo;Why aren&amp;rsquo;t you using Diesel?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;He was right. Sometimes you don&amp;rsquo;t want to write SQL. Sometimes you want the boilerplate to disappear.&lt;/p&gt;</description></item><item><title>Lesson 1: SQLx — Compile-time checked queries</title><link>/post/rust/rust-db-sqlx-intro/</link><pubDate>Sun, 20 Oct 2024 11:23:00 +0000</pubDate><guid>/post/rust/rust-db-sqlx-intro/</guid><description>&lt;p&gt;I shipped a typo in a SQL column name to production last year. The column was &lt;code&gt;user_nme&lt;/code&gt; instead of &lt;code&gt;user_name&lt;/code&gt;. The Go service compiled fine, the tests passed (they used mocks), and the bug sat in production for three hours before a customer reported it. Three hours of silent failures because the query returned zero rows instead of erroring out.&lt;/p&gt;
&lt;p&gt;That was the day I started using SQLx in Rust. I haven&amp;rsquo;t shipped a SQL typo since.&lt;/p&gt;</description></item></channel></rss>