Testing PostgreSQL 19 beta without guessing

A production-like dump, a separate PostgreSQL 19 beta instance, one evening plus machine time. That is the whole method, and it is the only thing a database beta is for: finding out where the new version changes the behavior of your application while the change is still a bug report and not your incident. Restore the dump first. Data size matters. A plan that is fine on a thousand rows goes bad on fifty million. ...

July 8, 2026 · 2 min · Murat Useinov

MySQL 9.7 LTS is an operational choice

An SQL mode set in 2019 that nobody remembers why. An authentication plugin the driver happened to support. A charset and collation nobody chose. That is what a MySQL upgrade is made of. MySQL 9.7 LTS is out, so I am making the list again. An LTS release of a database is a date more than a feature list. The date until which somebody else worries about patches, and the date after which the worry is yours. For a working PHP project the question is not “what is new”. It is “what did we assume”. ...

April 10, 2026 · 2 min · Murat Useinov

PostgreSQL 18, the upgrade notes

PostgreSQL 18 went GA yesterday. I ran the beta in July. Now it is not a preview, it is a ticket, and these are the notes from planning it. The quiet feature that matters most for the upgrade itself: pg_upgrade now carries planner statistics over. Before, the minutes or hours after switching versions were the scary part. Fresh cluster, empty statistics, the planner guessing, production limping until ANALYZE finished. That cliff is mostly gone. This alone changes how a major upgrade feels at 2 a.m. ...

September 26, 2025 · 2 min · Murat Useinov

A weekend with PostgreSQL 18 beta

A staging copy of one database, restored over the weekend onto PostgreSQL 18 beta. GA is expected in autumn. Two things I wanted to touch: asynchronous I/O and the built-in uuidv7(). Start with uuidv7, because it is about a mistake many of us already made, me included. Random UUIDv4 primary keys scatter inserts across the whole index. Every insert lands on a random page, the working set is the entire index, buffers churn. UUIDv7 is time-ordered: new rows go to the same few pages, like a sequence, but still globally unique. ...

July 2, 2025 · 2 min · Murat Useinov

PostgreSQL 17 and why VACUUM is an API concern

A reporting transaction open for hours, and n_dead_tup on the hot table climbing all day while autovacuum ran non-stop and changed nothing. That was one project. PostgreSQL 17 came out yesterday, and the headline work is exactly this plumbing: vacuum got a new memory structure for tracking dead tuples, far less memory and no longer capped the old way, plus better throughput for high-concurrency writes and bulk loads. Maintenance internals. Which is why backend people should care. ...

September 27, 2024 · 2 min · Murat Useinov

AI assistants do not know your production

A DTO with a dozen fields, a framework migration, a slow query incident. Three tasks from the last few months, all done with Copilot and ChatGPT open. Here is where the line runs for me. Task one, boilerplate. The DTO, its validation rules, a test with fixtures. The assistant writes this faster than I type, and the result needs only a quick read. Same for converting an array-shaped legacy structure into typed classes. This is honest time saved, maybe an hour a day. The code is the kind where being generic is correct. ...

December 23, 2023 · 2 min · Murat Useinov

PostgreSQL 16, read through one workload

A nightly import, a few hundred million rows through COPY, then reporting queries in the morning. That is the only workload I care about, so that is how I read the PostgreSQL 16 release notes: with one pipeline in mind and everything else skipped. Three items survived the filter. COPY got faster, noticeably when several loads run at once. Our import is exactly this shape, parallel COPY streams into partitioned tables. I will take the free speed. But the old lesson stays: if your import still does row-by-row INSERT from PHP, no Postgres release will save you. Batch into COPY first, then talk about versions. ...

September 16, 2023 · 2 min · Murat Useinov

UUID primary keys and the B-tree bill

The same table, the same rows, about 1.5 times the disk. That was the measurement after someone made UUIDv4 the primary key in InnoDB, and that is before counting secondary indexes. Every new API wants UUIDs. Distributed generation, no coordination, nothing leaks about row counts. All true. Then the database quietly starts paying. InnoDB clusters the table by primary key. Auto-increment inserts always land in the rightmost page, warm and predictable. Random UUIDs land anywhere in the tree: page splits everywhere, half-empty pages, and the set of hot pages becomes the whole index. Secondary indexes store the primary key in every entry, so each of them inherits the full 16 bytes too. In PostgreSQL the heap does not cluster, so the pain is smaller, but index bloat and lost locality are still there. ...

August 3, 2023 · 2 min · Murat Useinov

Outbox: events that do not lie

An email confirming an order that does not exist. A colleague showed me this one last week, together with the mirror bug: an order that exists and never got its email. The code was the obvious kind. Commit the Doctrine transaction, then dispatch to the AMQP transport. Two systems, two writes, no shared transaction. Publish after commit and the process can die between them, event lost. Publish inside the transaction and the broker gets an event for data that may still roll back. Both orders are wrong, and under real load this fires weekly. ...

June 7, 2023 · 2 min · Murat Useinov

MERGE arrives in PostgreSQL 15

A supplier price list, loaded into staging_prices, then reconciled against prices. Since PostgreSQL 15 came out two weeks ago, that is one statement: MERGE INTO prices p USING staging_prices s ON p.sku = s.sku WHEN MATCHED AND s.price IS NULL THEN DELETE WHEN MATCHED AND p.price <> s.price THEN UPDATE SET price = s.price, updated_at = now() WHEN NOT MATCHED THEN INSERT (sku, price) VALUES (s.sku, s.price); Before 15 this was three statements in a transaction, or a stored procedure, or a loop in PHP. Now it is one statement that says what it does. Update changed rows, delete withdrawn ones, insert new ones. People coming from Oracle and SQL Server waited a decade for this. The conditional WHEN MATCHED AND ... branches are the real value. ON CONFLICT cannot express “delete when the source says so” at all. ...

October 27, 2022 · 2 min · Murat Useinov