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

MySQL now has two speeds

MySQL 8.1 came out this week, and its support window is measured in months. That is by design. Oracle changed the release model: two tracks now. Innovation releases every quarter or so, 8.1 is the first, short support, new features fast. And an LTS line with years of support, promised for next year. 8.0 stays on bugfixes until its end of life in 2026. For years the question was “when do we finally move to 8.0”. Now the question has a second dimension: which track are you even on. That is a product decision, and it is worth making explicitly instead of by inertia. ...

July 20, 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

Scheduler component, or cron as messages

crontab -l under a login nobody remembers creating. That is where the real business logic of one server lived: a nightly cleanup, an export, a retry script. Symfony 6.3 came out yesterday with a Scheduler component. Periodic tasks defined in PHP, executed as Messenger messages. Experimental, but the idea deserves a note. #[AsSchedule('default')] final class MainSchedule implements ScheduleProviderInterface { public function getSchedule(): Schedule { return (new Schedule())->add( RecurringMessage::every('10 minutes', new CleanupExpiredCarts()), ); } } Then messenger:consume scheduler_default, and a worker fires the messages. ...

May 31, 2023 · 2 min · Murat Useinov

Persistent PHP workers need discipline

A static array with the comment “cache, cleared per request”. It went to production on RoadRunner last month, and the memory graph turned into a staircase. PHP had one superpower nobody advertised: the request died. Every leak, every forgotten static, every open transaction was erased when the process shut down. Shared-nothing was not architecture. It was amnesia, and amnesia forgave us everything. RoadRunner and Swoole take it away. The worker lives for thousands of requests, and the class of bugs changes on the first day. That “per request” cache became a cache per worker lifetime, and the supervisor killed the process when it ran out of memory. A logger kept the request id in a property, so entries from user B carried the id of user A. And the best one: an exception in the middle of a Doctrine transaction left the connection with the transaction open, and the next request on that worker silently joined it. ...

April 3, 2023 · 2 min · Murat Useinov

DI attributes: config moves into the class

Three lines of YAML in a file two directories away, for one integer that one service reads. That was the first thing I converted when I started moving a project’s service config from YAML into attributes, to see where the line is. public function __construct( #[Autowire('%env(int:IMPORT_BATCH_SIZE)%')] private int $batchSize, ) {} Now the class tells you everything about itself. Same with #[TaggedIterator] for collecting all implementations of an interface, and #[AsDecorator] for wrapping a service. These are local facts. A local fact belongs next to the code it describes. When I open the class, I want to stop searching. ...

March 27, 2023 · 2 min · Murat Useinov

Laravel 10 is a quiet major

Process::run('gzip -k dump.sql'). Out of the whole Laravel 10 release this week, that is the line I will actually use. The rest is quiet. Skeleton and framework code got native type declarations instead of docblocks, PHP 8.1 is the floor, and there is the Process facade. That is roughly the whole story. Some people are disappointed. I am not. A major release that is mostly maintenance means the framework is an adult. ...

February 16, 2023 · 2 min · Murat Useinov

Before Laravel 10: raise the baseline first

composer why-not php 8.1. One command, and it tells you who holds you back. Run it before making any plans. Laravel 10 comes next month and requires PHP 8.1. One project I help with is on Laravel 9 and PHP 8.0. The temptation is one heroic branch: new PHP, new framework, new package versions. When that branch breaks in production, you will not know which of the three changes broke it. ...

January 7, 2023 · 2 min · Murat Useinov

PHP 8.2: stricter by default

PHP 8.2 was released on December 8. My checklist for it looks like every other December, which is the whole point of a yearly cadence. On the surface a quiet release. Readonly classes, DNF types, standalone true, false and null types, constants in traits. Underneath, the more important half: another round of cleaning old dynamic behavior. Dynamic properties deprecated, ${var} string interpolation deprecated, utf8_encode deprecated. The language keeps trading looseness for predictability, and I keep voting for the trade. ...

December 16, 2022 · 2 min · Murat Useinov