<?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>Mysql on Murat Useinov</title>
    <link>https://useinov.com/tags/mysql/</link>
    <description>Recent content in Mysql on Murat Useinov</description>
    <generator>Hugo</generator>
    <language>en-us</language>
    <lastBuildDate>Thu, 11 Jun 2026 22:30:00 +0300</lastBuildDate>
    <atom:link href="https://useinov.com/tags/mysql/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>When version numbers stop talking</title>
      <link>https://useinov.com/posts/2026/calendar-versions/</link>
      <pubDate>Thu, 11 Jun 2026 22:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2026/calendar-versions/</guid>
      <description>&lt;p&gt;Major means breakage possible, minor means safe. For twenty years a MySQL version number carried that promise, and you could plan by reading it. With the move to calendar-style releases the number tells you one thing: when it was built. The promise moved elsewhere, and you have to know where to look.&lt;/p&gt;
&lt;p&gt;The signals now are the channel and the dates. Is this an LTS or an innovation release. When does support end. What do the compatibility notes actually say, because &amp;ldquo;no major version&amp;rdquo; does not mean &amp;ldquo;no behavior change&amp;rdquo;. Optimizer changes ship in any release. Your query plans do not read version numbers.&lt;/p&gt;</description>
    </item>
    <item>
      <title>MySQL 9.7 LTS is an operational choice</title>
      <link>https://useinov.com/posts/2026/mysql-97-lts/</link>
      <pubDate>Fri, 10 Apr 2026 20:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2026/mysql-97-lts/</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;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 &amp;ldquo;what is new&amp;rdquo;. It is &amp;ldquo;what did we assume&amp;rdquo;.&lt;/p&gt;</description>
    </item>
    <item>
      <title>MySQL 8.4 gives us a place to stand</title>
      <link>https://useinov.com/posts/2024/mysql-lts/</link>
      <pubDate>Tue, 30 Apr 2024 21:05:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2024/mysql-lts/</guid>
      <description>&lt;p&gt;Half the industry is sitting on MySQL 5.7 right now, unsupported since last autumn, and calling it stability.&lt;/p&gt;
&lt;p&gt;MySQL 8.4 came out today, and the important word in the announcement is not a feature. It is LTS. First long-term release of the new model: innovation releases every quarter for the brave, an LTS every couple of years for production, bugfixes only after that.&lt;/p&gt;
&lt;p&gt;People underestimate how much pain the old model caused. 8.0 was a rolling target. A &amp;ldquo;minor&amp;rdquo; 8.0.x could change optimizer behavior or deprecate something you used. Staying current meant re-testing, staying behind meant missing security fixes.&lt;/p&gt;</description>
    </item>
    <item>
      <title>UUID primary keys and the B-tree bill</title>
      <link>https://useinov.com/posts/2023/uuid-primary-keys/</link>
      <pubDate>Thu, 03 Aug 2023 21:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2023/uuid-primary-keys/</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;Every new API wants UUIDs. Distributed generation, no coordination, nothing leaks about row counts. All true. Then the database quietly starts paying.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;</description>
    </item>
    <item>
      <title>MySQL now has two speeds</title>
      <link>https://useinov.com/posts/2023/mysql-release-tracks/</link>
      <pubDate>Thu, 20 Jul 2023 22:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2023/mysql-release-tracks/</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;For years the question was &amp;ldquo;when do we finally move to 8.0&amp;rdquo;. 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.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Nobody closes your database connection anymore</title>
      <link>https://useinov.com/posts/2021/long-lived-connections/</link>
      <pubDate>Tue, 20 Jul 2021 23:20:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2021/long-lived-connections/</guid>
      <description>&lt;p&gt;&amp;ldquo;MySQL server has gone away&amp;rdquo;, first thing in the morning, on a worker that had no traffic all night. That was week one after we moved a project from FPM to RoadRunner.&lt;/p&gt;
&lt;p&gt;For all these years FPM managed our database connections. Not with clever code. By dying. Process ends, connection closes, transaction rolls back, session variables reset. Nobody thought about connection lifecycle, because there was none.&lt;/p&gt;
&lt;p&gt;Move the same code to RoadRunner or Swoole workers and the topic is back. A connection lives for hours now. Two things go wrong, and both showed up within a week.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Stop building running totals in PHP</title>
      <link>https://useinov.com/posts/2020/window-functions-mysql/</link>
      <pubDate>Tue, 06 Oct 2020 15:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2020/window-functions-mysql/</guid>
      <description>&lt;p&gt;Two hundred thousand rows over the wire to compute forty numbers. That was our monthly report on MySQL 5.7: fetch all transactions for the period, loop in PHP, accumulate a running balance, compare each row with the previous one, rank customers by volume. Classic 5.7 shape, because the database could not say &amp;ldquo;previous row&amp;rdquo; or &amp;ldquo;rank within group&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;We are finally moving that project to MySQL 8. Two years after GA, which by database standards is reckless haste. The first win had nothing to do with performance. We deleted PHP.&lt;/p&gt;</description>
    </item>
    <item>
      <title>A deadlock is the database doing its job</title>
      <link>https://useinov.com/posts/2020/deadlock-not-bug/</link>
      <pubDate>Sat, 02 May 2020 20:30:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2020/deadlock-not-bug/</guid>
      <description>&lt;p&gt;Error 1213 in the logs, and the chat lights up: the database is broken. It is not. Two transactions locked rows in opposite order, A waits for B, B waits for A, and InnoDB did the only sane thing: picked a victim and killed it. The cycle is gone. This is a feature.&lt;/p&gt;
&lt;p&gt;The classic shape is a money transfer. One request moves funds from account 1 to account 2, another from 2 to 1, both update the first account and then the second. Opposite order, instant cycle under load. Tests never show it, because tests do not run two of these in the same millisecond.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Transactional DDL is not portable</title>
      <link>https://useinov.com/posts/2019/transactional-ddl/</link>
      <pubDate>Mon, 18 Mar 2019 22:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2019/transactional-ddl/</guid>
      <description>&lt;p&gt;Statement three of a migration fails. Column name typo. On PostgreSQL the first two ALTERs roll back with it, the schema returns to the exact state before the migration, you fix the typo and run again. On MySQL the first two ALTERs are already permanent.&lt;/p&gt;
&lt;p&gt;Same &lt;code&gt;up()&lt;/code&gt; method, same &lt;code&gt;php artisan migrate&lt;/code&gt;, same green output. The migration tool gives one abstraction over two very different databases. The abstraction covers syntax. It does not cover what happens on failure.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Covering index and the price of SELECT *</title>
      <link>https://useinov.com/posts/2018/covering-index/</link>
      <pubDate>Sun, 15 Jul 2018 12:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2018/covering-index/</guid>
      <description>&lt;p&gt;&lt;code&gt;Using index&lt;/code&gt; in the Extra column of EXPLAIN. That is the cheapest read MySQL can do, and you often get it almost for free.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&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;user_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;n&#34;&gt;created_at&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&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;
&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;user_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;mi&#34;&gt;42&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&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;created_at&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&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;20&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With an index on &lt;code&gt;(user_id, created_at)&lt;/code&gt; everything the query needs is in the index leaves. The table is never touched. A composite index finds rows fast, a covering index answers the query on its own. Postgres calls this Index Only Scan, with one condition: the visibility map must be fresh, so a table that vacuum never visits quietly falls back to heap fetches.&lt;/p&gt;</description>
    </item>
    <item>
      <title>MySQL 8.0: finally, modern SQL</title>
      <link>https://useinov.com/posts/2018/mysql-eight-sql/</link>
      <pubDate>Tue, 24 Apr 2018 22:15:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2018/mysql-eight-sql/</guid>
      <description>&lt;p&gt;&lt;code&gt;@row := @row + 1&lt;/code&gt;. I have a report in production that stands on that trick, and the trick was never guaranteed to work. Evaluation order of user variables in SELECT is undefined, it just happened to behave. MySQL 8.0 went GA last week, and for me the release is about SQL. Window functions and CTEs, the things Postgres people stopped noticing years ago, are here.&lt;/p&gt;
&lt;p&gt;The classic task: top three orders per customer. In 5.7 you had a self-join nobody could read a month later, or the variable trick. Now:&lt;/p&gt;</description>
    </item>
    <item>
      <title>ALTER TABLE is part of the deploy</title>
      <link>https://useinov.com/posts/2017/zero-downtime-migrations/</link>
      <pubDate>Mon, 24 Apr 2017 15:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2017/zero-downtime-migrations/</guid>
      <description>&lt;p&gt;Forty million rows, three application instances behind a balancer, one migration that renames a column. The deploy script runs migrations first, then rolls the code instance by instance. For a few minutes old code was writing to a column that no longer existed. The error rate graph looked like a wall. I had approved that migration. It was one line.&lt;/p&gt;
&lt;p&gt;Migration tools trained us to think a schema change is a code change. Write a file, run it, done. A schema change on a big table takes time, takes locks, and while it runs the previous version of the application is still serving traffic. Both versions must survive both schemas. That is the whole discipline, and it has a name: expand and contract.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Column order in a composite index</title>
      <link>https://useinov.com/posts/2016/composite-index-order/</link>
      <pubDate>Mon, 04 Jul 2016 23:25:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2016/composite-index-order/</guid>
      <description>&lt;p&gt;Fifty rows. Three indexes on the table, one per column. Still slow. The query is the standard one from any multi-tenant application:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&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;o&#34;&gt;*&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;orders&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&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;tenant_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;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;AND&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;status&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;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&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;created_at&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&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;50&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&amp;ldquo;Add an index&amp;rdquo; is the typical reaction, and one index on each column is the typical result. The number of indexes is not the point. The order of columns inside one index is.&lt;/p&gt;</description>
    </item>
    <item>
      <title>JSON column is not a schema</title>
      <link>https://useinov.com/posts/2015/json-not-schema/</link>
      <pubDate>Fri, 23 Oct 2015 22:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2015/json-not-schema/</guid>
      <description>&lt;div class=&#34;highlight&#34;&gt;&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;ALTER&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;k&#34;&gt;ADD&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;profile&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;JSON&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;MySQL 5.7 went GA this week, and this line is now legal. Native JSON type: validation on insert, binary storage, functions to read paths. The question is not whether it works. It is what belongs in it.&lt;/p&gt;
&lt;p&gt;Classic case: user profile with flexible metadata. Marketing wants a new field every other week. The old options were bad in familiar ways. Forty nullable columns and an ALTER for every idea. Or EAV, key-value rows, where every read is a self-join festival and nothing has a type.&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
