<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>my tech blog &#187; MySQL</title>
	<atom:link href="http://billauer.se/blog/category/mysql/feed/" rel="self" type="application/rss+xml" />
	<link>https://billauer.se/blog</link>
	<description>Anything I found worthy to write down.</description>
	<lastBuildDate>Thu, 12 Mar 2026 11:36:00 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.1.2</generator>
		<item>
		<title>Perl + DBI: Measuring the time of database queries</title>
		<link>https://billauer.se/blog/2021/01/perl-mysql-query-time-elapsed/</link>
		<comments>https://billauer.se/blog/2021/01/perl-mysql-query-time-elapsed/#comments</comments>
		<pubDate>Wed, 06 Jan 2021 11:00:00 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[perl]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=6210</guid>
		<description><![CDATA[It&#8217;s often desired to know how much wall clock time the SQL queries take. As with Perl, there&#8217;s more than one way to do it. This is a simple way, which involves overriding DBI&#8217;s execute() method, so it measures the time and issues a warn() with basic caller info and the time in milliseconds. The [...]]]></description>
			<content:encoded><![CDATA[<p>It&#8217;s often desired to know how much wall clock time the SQL queries take. As with Perl, there&#8217;s more than one way to do it. This is a simple way, which involves overriding DBI&#8217;s execute() method, so it measures the time and issues a warn() with basic caller info and the time in milliseconds.</p>
<p>The same thing can be done with any other method, of course. Go &#8220;man DBI&#8221; and look for &#8220;Subclassing the DBI&#8221; for the full story.</p>
<p>So the first thing is to define the RootClass property on the DBI object generation, so that MySubDBI is the root class. Something like</p>
<pre>$dbh = DBI-&gt;connect( "DBI:mysql::localhost", "thedatabase", "thepassword",
		     { RaiseError =&gt; 1, AutoCommit =&gt; 1, PrintError =&gt; 0,
		      <span style="color: #ff0000;"><strong> RootClass =&gt; 'MySubDBI',</strong></span>
		       Taint =&gt; 1});</pre>
<p>and then, somewhere, the class needs to be defined. This can be in a separate module .pm file, but also at the end of the same file as the code for the DBI-&gt;connect:</p>
<pre>package MySubDBI;
use strict;

use DBI;
use vars qw(@ISA);
@ISA = qw(DBI);

package MySubDBI::db;
use vars qw(@ISA);
@ISA = qw(DBI::db);

# Empty, yet necessary.

package MySubDBI::st;
use Time::HiRes qw(gettimeofday tv_interval);
use vars qw(@ISA);
@ISA = qw(DBI::st);

sub execute {
  my ($sth, @args) = @_;
  my $tic = [gettimeofday];
  my $res = $sth-&gt;SUPER::execute(@args);
  my $exectime = int(tv_interval($tic)*1000);

  my ($package0, $file0, $line0, $subroutine0) = caller(0);
  my ($package1, $file1, $line1, $subroutine1) = caller(1);

  warn("execute() call from $subroutine1 (line $line0) took $exectime ms\n");
  return $res;
}

1;</pre>
<p>The code can be smarter, of course. For example, issue a warning only if the query time exceeds a certain limit.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2021/01/perl-mysql-query-time-elapsed/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>MySQL: LIKE vs. REGEXP() performance testing</title>
		<link>https://billauer.se/blog/2020/12/mysql-index-pattern-matching-performance/</link>
		<comments>https://billauer.se/blog/2020/12/mysql-index-pattern-matching-performance/#comments</comments>
		<pubDate>Tue, 29 Dec 2020 09:38:11 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[MySQL]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=6207</guid>
		<description><![CDATA[Introduction At some point I needed to choose between using LIKE or REGEXP() for a not so simple string match. Without going into the details, the matching string contains a lot of wildcard segments, and while both would have done the job, I thought maybe REGEXP() would benefit from some extra information about the wildcard [...]]]></description>
			<content:encoded><![CDATA[<h3>Introduction</h3>
<p>At some point I needed to choose between using LIKE or REGEXP() for a not so simple string match. Without going into the details, the matching string contains a lot of wildcard segments, and while both would have done the job,  I thought maybe REGEXP() would benefit from some extra information about the wildcard parts. It&#8217;s not that I cared that &#8216;%&#8217; would match characters it shouldn&#8217;t, but I wanted so save some backtracking by telling the matching engine not to match just anything. Save some CPU, I thought. Spoiler: It was a nice thought, but no.</p>
<p>So I ran a few performance tests on a sample table:</p>
<pre>CREATE TABLE product_info (
	cpe_name BLOB NOT NULL,
	title BLOB NOT NULL,
	PRIMARY KEY (cpe_name(511))
) engine = MyISAM;</pre>
<p>Note that cpe_name is the primary key, and is hence a unique index.</p>
<p>I should also mention that the match patterns I use are for testing are practically useless for CPE name matching because they don&#8217;t handle escaped &#8220;:&#8221; characters properly. Just in case you know what a CPE is and you&#8217;re here for that. In short, these are just performance tests.</p>
<p>I did this on MySQL server version: 5.1.47, Source distribution. There are newer versions around, I know. Maybe they do better.</p>
<h3>The art of silly queries</h3>
<p>So there&#8217;s about more than half a million entries:</p>
<pre>mysql&gt; SELECT COUNT(*) FROM product_info;
+----------+
| COUNT(*) |
+----------+
|   588094 |
+----------+
1 row in set (0.00 sec)</pre>
<p>Let&#8217;s ask it another way:</p>
<pre>mysql&gt; SELECT COUNT(*) FROM product_info WHERE cpe_name LIKE '%';
+----------+
| COUNT(*) |
+----------+
|   588094 |
+----------+
1 row in set (<strong>0.08 sec</strong>)</pre>
<p>Say what? LIKE &#8216;%&#8217; is always true for a non-NULL BLOB. MySQL didn&#8217;t optimize this simple thing, and actually checked every single entry?</p>
<pre>mysql&gt; EXPLAIN SELECT COUNT(*) FROM product_info;
+----+-------------+-------+------+---------------+------+---------+------+------+------------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra                        |
+----+-------------+-------+------+---------------+------+---------+------+------+------------------------------+
|  1 | SIMPLE      | NULL  | NULL | NULL          | NULL | NULL    | NULL | NULL | Select tables optimized away |
+----+-------------+-------+------+---------------+------+---------+------+------+------------------------------+
1 row in set (0.00 sec)

mysql&gt; EXPLAIN SELECT COUNT(*) FROM product_info WHERE cpe_name LIKE '%';
+----+-------------+--------------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table        | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+--------------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | product_info | ALL  | NULL          | NULL | NULL    | NULL | 588094 | Using where |
+----+-------------+--------------+------+---------------+------+---------+------+--------+-------------+
1 row in set (0.00 sec)</pre>
<p>Apparently it did. Other silly stuff:</p>
<pre>mysql&gt; SELECT COUNT(*) FROM product_info WHERE NOT cpe_name IS NULL;
+----------+
| COUNT(*) |
+----------+
|   588094 |
+----------+
1 row in set (<strong>0.08 sec</strong>)

mysql&gt; EXPLAIN SELECT COUNT(*) FROM product_info WHERE NOT cpe_name IS NULL;
+----+-------------+--------------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table        | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+--------------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | product_info | ALL  | PRIMARY       | NULL | NULL    | NULL | 588094 | Using where |
+----+-------------+--------------+------+---------------+------+---------+------+--------+-------------+
1 row in set (0.00 sec)</pre>
<p>Silly or what? The column is defined as &#8220;NOT NULL&#8221;. What is there to check? So maybe the idea is that if I make stupid queries, MySQL responds with stupid behavior. Well, not really:</p>
<pre>mysql&gt; SELECT COUNT(*) FROM product_info WHERE 1=1;
+----------+
| COUNT(*) |
+----------+
|   588094 |
+----------+
1 row in set (0.00 sec)

mysql&gt; EXPLAIN SELECT COUNT(*) FROM product_info WHERE 1=1;
+----+-------------+-------+------+---------------+------+---------+------+------+------------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra                        |
+----+-------------+-------+------+---------------+------+---------+------+------+------------------------------+
|  1 | SIMPLE      | NULL  | NULL | NULL          | NULL | NULL    | NULL | NULL | Select tables optimized away |
+----+-------------+-------+------+---------------+------+---------+------+------+------------------------------+
1 row in set (0.00 sec)</pre>
<p>It&#8217;s more like MySQL makes optimizations only if they&#8217;re really obvious. MySQL != gcc.</p>
<h3>LIKE vs REGEXP</h3>
<p>So it turns out that LIKE is really fast, and it seems to take advantage of the index:</p>
<pre>mysql&gt; SELECT cpe_name FROM product_info WHERE cpe_name LIKE 'cpe:2.3:a:hummingbird:cyberdocs:-%';
+-------------------------------------------------+
| cpe_name                                        |
+-------------------------------------------------+
| cpe:2.3:a:hummingbird:cyberdocs:-:*:*:*:*:*:*:* |
+-------------------------------------------------+
1 row in set (0.00 sec)</pre>
<p>The same query, only with a regex:</p>
<pre>mysql&gt; SELECT cpe_name FROM product_info WHERE cpe_name REGEXP '^cpe:2.3:a:hummingbird:cyberdocs:-.*';
+-------------------------------------------------+
| cpe_name                                        |
+-------------------------------------------------+
| cpe:2.3:a:hummingbird:cyberdocs:-:*:*:*:*:*:*:* |
+-------------------------------------------------+
1 row in set <strong>(0.21 sec)</strong></pre>
<p>Recall that indexing means that the rows are sorted. Because the initial part of the string is fixed, it&#8217;s possible to narrow down the number of rows to match with an index lookup, and then work on those that match that initial part. This was taken advantage of with the LIKE match, but apparently not with REGEXP():</p>
<pre>mysql&gt; EXPLAIN SELECT cpe_name FROM product_info WHERE cpe_name LIKE 'cpe:2.3:a:hummingbird:cyberdocs:-%';
+----+-------------+--------------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table        | type  | possible_keys | key     | key_len | ref  | rows | Extra       |
+----+-------------+--------------+-------+---------------+---------+---------+------+------+-------------+
|  1 | SIMPLE      | product_info | <strong>range</strong> | PRIMARY       | <span style="color: #ff0000;"><strong>PRIMARY</strong></span> | 513     | NULL |    1 | Using where |
+----+-------------+--------------+-------+---------------+---------+---------+------+------+-------------+
1 row in set (0.00 sec)

mysql&gt; EXPLAIN SELECT cpe_name FROM product_info WHERE cpe_name REGEXP '^cpe:2.3:a:hummingbird:cyberdocs:-.*';
+----+-------------+--------------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table        | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+--------------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | product_info | <strong>ALL</strong>  | NULL          | <span style="color: #ff0000;"><strong>NULL</strong></span> | NULL    | NULL | 588094 | Using where |
+----+-------------+--------------+------+---------------+------+---------+------+--------+-------------+
1 row in set (0.00 sec)</pre>
<p>The conclusion is quite clear: For heavy duty matching, don&#8217;t use REGEXP() if LIKE can do the job. In particular if a lot of rows can be ruled out by virtue of the first characters in the string.</p>
<h3>Making it harder for LIKE</h3>
<p>Let&#8217;s warm up a bit:</p>
<pre>mysql&gt; SELECT cpe_name FROM product_info WHERE cpe_name LIKE 'cpe:2.3:a:<span style="color: #ff0000;"><strong>humming%</strong></span>:cyberdocs:-%';
+-------------------------------------------------+
| cpe_name                                        |
+-------------------------------------------------+
| cpe:2.3:a:hummingbird:cyberdocs:-:*:*:*:*:*:*:* |
+-------------------------------------------------+
1 row in set (0.00 sec)</pre>
<p>It was quicker than measurable, mainly because there was little to do:</p>
<pre>mysql&gt; EXPLAIN SELECT cpe_name FROM product_info WHERE cpe_name LIKE 'cpe:2.3:a:<span style="color: #ff0000;"><strong>humming%</strong></span>:cyberdocs:-%';
+----+-------------+--------------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table        | type  | possible_keys | key     | key_len | ref  | rows | Extra       |
+----+-------------+--------------+-------+---------------+---------+---------+------+------+-------------+
|  1 | SIMPLE      | product_info | <strong>range</strong> | PRIMARY       | <span style="color: #ff0000;"><strong>PRIMARY</strong></span> | 513     | NULL |   <span style="color: #ff0000;"><strong>80</strong></span> | Using where |
+----+-------------+--------------+-------+---------------+---------+---------+------+------+-------------+
1 row in set (0.00 sec)</pre>
<p>As expected, the index was used for the initial part of the string, and that left MySQL with 80 rows to actually do the matching (in this specific case). So it was quick.</p>
<p>But what if the wildcard is at the beginning of the string?</p>
<pre>mysql&gt; SELECT cpe_name FROM product_info WHERE cpe_name LIKE '%cpe:2.3:a:hummingbird:cyberdocs:-%';
+-------------------------------------------------+
| cpe_name                                        |
+-------------------------------------------------+
| cpe:2.3:a:hummingbird:cyberdocs:-:*:*:*:*:*:*:* |
+-------------------------------------------------+
1 row in set <strong>(0.10 sec)</strong></pre>
<p>So that took some considerable time, however still less than the REXEXP() case. The index didn&#8217;t help in this case, it seems:</p>
<pre>mysql&gt; EXPLAIN SELECT cpe_name FROM product_info WHERE cpe_name LIKE '%cpe:2.3:a:hummingbird:cyberdocs:-%';
+----+-------------+--------------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table        | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+--------------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | product_info | ALL  | NULL          | <span style="color: #ff0000;"><strong>NULL</strong></span> | NULL    | NULL | 588094 | Using where |
+----+-------------+--------------+------+---------------+------+---------+------+--------+-------------+
1 row in set (0.00 sec)</pre>
<p>So apparently, the pattern was applied to all rows. It was still faster than REGEXP() making a very simple match. So the latter seems not to be optimized in MySQL.</p>
<p>And to wrap this up, an expression full with wildcards. Similar to the one I thought maybe REXEXP would do better:</p>
<pre>mysql&gt; SELECT COUNT(*) FROM product_info WHERE cpe_name LIKE 'cpe:2.3:a:%:%:%:-:%:%:%:%:%:%';
+----------+
| COUNT(*) |
+----------+
|    13205 |
+----------+
1 row in set (<strong>0.11 sec</strong>)

mysql&gt; EXPLAIN SELECT COUNT(*) FROM product_info WHERE cpe_name LIKE 'cpe:2.3:a:%:%:%:-:%:%:%:%:%:%';
+----+-------------+--------------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table        | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+--------------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | product_info | ALL  | PRIMARY       | NULL | NULL    | NULL | 588094 | Using where |
+----+-------------+--------------+------+---------------+------+---------+------+--------+-------------+
1 row in set (0.00 sec)</pre>
<p>So what happened here is that the initial part of the string didn&#8217;t help much, and the match seems to have been done on all rows. It took more or less the same time as the much simpler match pattern above.</p>
<h3>Conclusion</h3>
<p>There seems to be two main conclusions from this little set of experiments: The first one isn&#8217;t surprising: The most important factor is how many rows are being accessed, not so much what is done with them. And the second is that MySQL does some sensible optimizations when LIKE is used, in particular it narrows down the number of rows with the index, when possible. Something it won&#8217;t do with REGEXP(), even if with an &#8220;^&#8221; at the beginning of the regex.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2020/12/mysql-index-pattern-matching-performance/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>MySQL: Enforcing long unique BLOB / TEXT columns with UNIQUE INDEX</title>
		<link>https://billauer.se/blog/2020/12/mysql-unique-index-blob-text/</link>
		<comments>https://billauer.se/blog/2020/12/mysql-unique-index-blob-text/#comments</comments>
		<pubDate>Tue, 22 Dec 2020 04:30:27 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[MySQL]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=6201</guid>
		<description><![CDATA[Introduction When creating indexes to TEXT / BLOB columns (and their variants), it&#8217;s required to specify how many characters the index should cover. In MySQL&#8217;s docs it&#8217;s usually suggested to keep them short in for better performance. There&#8217;s also a limit on the number of characters, which varies from one database engine to another, going [...]]]></description>
			<content:encoded><![CDATA[<h3>Introduction</h3>
<p>When creating indexes to TEXT / BLOB columns (and their variants), it&#8217;s required to specify how many characters the index should cover. In MySQL&#8217;s docs it&#8217;s usually suggested to keep them short in for better performance. There&#8217;s also a limit on the number of characters, which varies from one database engine to another, going from a few hundreds to a few thousands characters.</p>
<p>However it&#8217;s not unusual to use UNIQUE INDEX for making the database enforce the uniqueness of a field in the table. ON DUPLICATE KEY, INSERT IGNORE, UPDATE IGNORE and REPLACE can then be used to gracefully keep things tidy.</p>
<p>But does UNIQUE INDEX mean that the entire field remains unique, or is only the part covered by the index checked? Spoiler: Only the part covered by the index. In other words, MySQL&#8217;s uniqueness enforcement may be <strong>too strict</strong>.</p>
<p>Almost needless to say, if the index isn&#8217;t UNIQUE, it&#8217;s just a performance issue: If the number of covered characters is small, the database will more often fetch and discard the data of a row that turns out not to match the search criteria. But it doesn&#8217;t change the logic behavior. With UNIQUE INDEX, the number of characters does.</p>
<p>A simple test follows.</p>
<h3>Trying the UNIQUE INDEX</h3>
<p>I created a table with MySQL 5.1.47, with an index covering only 5 chars. Deliberately very short.</p>
<pre>CREATE TABLE try (
	id INT UNSIGNED NOT NULL AUTO_INCREMENT,
	message BLOB NOT NULL,
	PRIMARY KEY (id),
	UNIQUE INDEX (message(5))
) engine = MyISAM;</pre>
<p>Which ends up with this:</p>
<pre>mysql&gt; DESCRIBE try;
+---------+------------------+------+-----+---------+----------------+
| Field   | Type             | Null | Key | Default | Extra          |
+---------+------------------+------+-----+---------+----------------+
| id      | int(10) unsigned | NO   | PRI | NULL    | auto_increment |
| message | blob             | NO   | UNI | NULL    |                |
+---------+------------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)</pre>
<p>Inserting the first row:</p>
<pre>mysql&gt; INSERT INTO try(message) VALUES('Hello, world');
Query OK, 1 row affected (0.00 sec)</pre>
<p>And now trying a second:</p>
<pre>mysql&gt; INSERT INTO try(message) VALUES('Hello there');
ERROR 1062 (23000): Duplicate entry 'Hello' for key 'message'</pre>
<p>That&#8217;s it. It just looked at the first five chars. Trying a difference within this region:</p>
<pre>mysql&gt; INSERT INTO try(message) VALUES('Hell there');
Query OK, 1 row affected (0.00 sec)</pre>
<p>No wonder, that worked.</p>
<pre>mysql&gt; SELECT * FROM try;
+----+--------------+
| id | message      |
+----+--------------+
|  1 | Hello, world |
|  2 | Hell there   |
+----+--------------+
2 rows in set (0.00 sec)</pre>
<h3>Handling large TEXT/BLOB</h3>
<p>And that leaves the question: How is it possible to ensure uniqueness on large chunks of text or binary data? One solution I can think of is to add a column for a hash (say SHA1), and let the application calculate that hash for each row it inserts, and insert it along with it. And make the UNIQUE INDEX on the hash, not the text. Something like</p>
<pre>CREATE TABLE try2 (
	id INT UNSIGNED NOT NULL AUTO_INCREMENT,
	message BLOB NOT NULL,
<span style="color: #ff0000;"><strong>	hash TINYBLOB NOT NULL,
</strong></span>	PRIMARY KEY (id),
<span style="color: #ff0000;"><strong>	UNIQUE INDEX (hash(40))
</strong></span>) engine = MyISAM;</pre>
<p>But wait. MySQL supports hashing functions. Why not use them instead? Well, the problem is that if I want an INSERT statement push the data and its hash in one go they query becomes a bit nasty. What came to my mind is:</p>
<pre>mysql&gt; INSERT INTO try2(message, hash) (SELECT t.x, SHA1(t.x) FROM (SELECT 'Hello there' AS x) AS t);
Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0</pre>
<p>Can it get more concise than this? Suggestions are welcome. The double SELECT is required because I want the string literal to be mentioned once.</p>
<p>Isn&#8217;t it easier to let the application calculate the SHA1, and send it to the server by value? It&#8217;s a matter of taste, I guess.</p>
<p>Anyhow, trying again with exactly the same:</p>
<pre>mysql&gt; INSERT INTO try2(message, hash) (SELECT t.x, SHA1(t.x) FROM (SELECT 'Hello there' AS x) AS t);
ERROR 1062 (23000): Duplicate entry '726c76553e1a3fdea29134f36e6af2ea05ec5cce' for key 'hash'</pre>
<p>and with something slightly different:</p>
<pre>mysql&gt; INSERT INTO try2(message, hash) (SELECT t.x, SHA1(t.x) FROM (SELECT 'Hello there!' AS x) AS t);
Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0</pre>
<p>So yep, it works:</p>
<pre>mysql&gt; SELECT * FROM try2;
+----+--------------+------------------------------------------+
| id | message      | hash                                     |
+----+--------------+------------------------------------------+
|  1 | Hello there  | 726c76553e1a3fdea29134f36e6af2ea05ec5cce |
|  2 | Hello there! | 6b19cb3790b6da8f7c34b4d8895d78a56d078624 |
+----+--------------+------------------------------------------+
2 rows in set (0.00 sec)</pre>
<p>Once again, even though the example I showed demonstrates how to make MySQL calculate the hash, I would do it in the application.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2020/12/mysql-unique-index-blob-text/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Perl, DBI and MySQL wrongly reads zeros from database</title>
		<link>https://billauer.se/blog/2019/02/perl-mysql-zeroes-taint/</link>
		<comments>https://billauer.se/blog/2019/02/perl-mysql-zeroes-taint/#comments</comments>
		<pubDate>Fri, 15 Feb 2019 15:29:37 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Internet]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[perl]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=5663</guid>
		<description><![CDATA[TL;DR: SELECT queries in Perl for numerical columns suddenly turned to zeros after a software upgrade. This is a really peculiar problem I had after my web hosting provider upgraded some database related software on the server: Numbers that were read with SELECT queries from the database were suddenly all zeros. Spoiler: It&#8217;s about running [...]]]></description>
			<content:encoded><![CDATA[<p>TL;DR: SELECT queries in Perl for numerical columns suddenly turned to zeros after a software upgrade.</p>
<p>This is a really peculiar problem I had after my web hosting provider upgraded some database related software on the server: Numbers that were read with SELECT queries from the database were suddenly all zeros.</p>
<p>Spoiler: It&#8217;s about running Perl in Taint Mode.</p>
<p>The setting was DBD::mysql version 4.050, DBI version 1.642, Perl v5.10.1, and MySQL Community Server version 5.7.25 on a Linux machine.</p>
<p>For example, the following script is supposed to write the number of lines in the &#8220;session&#8221; table:</p>
<pre>#!/usr/bin/perl -T -w
use warnings;
use strict;
require DBI;

my $dbh = DBI-&gt;connect( "DBI:mysql:mydb:localhost", "mydb", "password",
		     { RaiseError =&gt; 1, AutoCommit =&gt; 1, PrintError =&gt; 0,
		       Taint =&gt; 1});

my $sth = $dbh-&gt;prepare("SELECT COUNT(*) FROM session");

$sth-&gt;execute();

my @l = $sth-&gt;fetchrow_array;
my $s = $l[0];
print "$s\n";

$sth-&gt;finish();
$dbh-&gt;disconnect;</pre>
<p>But instead, it prints zero, even though there are rows in the said table. Turning off taint mode by removing the &#8220;-T&#8221; flag in the shebang line gives the correct output. Needless to say, accessing the database with the &#8220;mysql&#8221; command-line utility client gave the correct output as well.</p>
<p>This is true for any numeric readout from this MySQL wrapper. This is in particular problematic when an integer is used as a user ID of a web site, and fetched with</p>
<pre>my $sth = db::prepare_cached("SELECT id FROM users WHERE username=? AND passwd=?");
$sth-&gt;execute($name, $password);
my ($uid) = $sth-&gt;fetchrow_array;
$sth-&gt;finish();</pre>
<p>If the credentials are wrong, $uid will be undef, as usual. But if <strong>any valid user</strong> gives correct credentials, it&#8217;s allocated user number 0. Which I was cautious enough not to allocate as the site&#8217;s supervisor, but that&#8217;s actually a common choice (what&#8217;s the UID of root on a Linux system?).</p>
<p>A softer workaround, instead of dropping the &#8220;-T&#8221; flag,  is to set the TaintIn flag in the DBI-&gt;connect() call, instead of Taint. The latter stands for TaintIn and TaintOut, and so this fix effectively disables TaintOut, hence tainting of data from the database is disabled. And in this case, disabling tainting of this data also skips the zero-value bug. This leaves all other tainting checks in place, in particular that of data supplied from the network. So not enforcing sanitizing data from the database is a small sacrifice (in particular if the script already has mileage running with the enforcement on).</p>
<p>And in the end I wonder if I&#8217;m the only one who uses Perl&#8217;s tainting mechanism. I mean, if there are still (2019) advisories on SQL injections (mostly PHP scripts), maybe people just don&#8217;t care much about things of this sort.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2019/02/perl-mysql-zeroes-taint/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Why MySQL&#8217;s (SQL) DATETIME can and should be avoided</title>
		<link>https://billauer.se/blog/2009/03/mysql-datetime-epoch-unix-time/</link>
		<comments>https://billauer.se/blog/2009/03/mysql-datetime-epoch-unix-time/#comments</comments>
		<pubDate>Sun, 15 Mar 2009 14:58:24 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Software]]></category>
		<category><![CDATA[datetime]]></category>
		<category><![CDATA[epoch]]></category>
		<category><![CDATA[javascript]]></category>
		<category><![CDATA[perl]]></category>
		<category><![CDATA[python]]></category>
		<category><![CDATA[time]]></category>
		<category><![CDATA[unix]]></category>
		<category><![CDATA[utc]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=34</guid>
		<description><![CDATA[I warmly recommend reading the comments at the bottom of this page, many of which go against my point. While I still stand behind every word I said, in particular for web applications (which I believe is the vast majority of MySQL use), the comments below make some valid points, and single out cases where [...]]]></description>
			<content:encoded><![CDATA[<div style="border: 2px solid #9b9; margin: 20px 5px 50px 5px; padding: 15px 5px 0 5px;"><i></p>
<p>I warmly recommend reading the comments at the bottom of this page, many of which go against my point. While I still stand behind every word I said, in particular for web applications (which I believe is the vast majority of MySQL use), the comments below make some valid points, and single out cases where DATETIME actually <b>is</b> the right thing.</p>
<p>
Needless to say, this is a discussion, and we&#8217;re all free to make our own mistakes.</p>
<p></i></div>
<h2>SQL DATETIME sucks</h2>
<p>MySQL, among other databases, has a column type called DATETIME. Its name seems to mislead people into thinking that it&#8217;s suitable for storing time of events. Or suitable for anything.</p>
<p>This is a general SQL thing, by the way, but I&#8217;ll demonstrate it on MySQL.</p>
<p>I often find this column type in other people&#8217;s database schemas, and I wonder if the designer gave it a thought before using it. It&#8217;s true, that in the beginning it looks simple:</p>
<pre>mysql&gt; CREATE TABLE stupid_date ( thedate DATETIME, PRIMARY KEY (thedate) );
Query OK, 0 rows affected (0.04 sec)

mysql&gt; INSERT INTO stupid_date(thedate) VALUES ( NOW() );
Query OK, 1 row affected (0.03 sec)

mysql&gt; SELECT * FROM stupid_date;
+---------------------+
| thedate             |
+---------------------+
| 2009-03-15 14:01:43 |
+---------------------+
1 row in set (0.00 sec)</pre>
<p>That was way too cute, wasn&#8217;t it? We also have the NOW() function, which fits in exactly, and puts the current time! Yay! Yay! And if the timestamp looks old-fashioned to you, I suppose there is a reason for that.</p>
<p>But wait, there are two major problems. The first one is that the time is given in the host&#8217;s local time. That was fair enough before the internet was invented. But today a web server can be a continent away. DATETIME will show you the local time of the server, not yours. There are SQL functions to convert timezones, of course. Are you sure that you want to deal with them? What happens when you want to move your database to a server in another timezone? What about daylight saving time? Local time is one big YUCK.</p>
<p><em>(Update: As commented below, the real stupidity is to use NOW(), and not UTC_TIMESTAMP(). The latter gives the UTC time, as its name implies)</em></p>
<p>Problem number two: Most applications don&#8217;t care what the absolute time is. The current time is commonly used to calculate how much time has elapsed since a certain event. To filter elements according to if they were X seconds before now. Is the user still logged in? Has 24 hours elapsed since the last warning email was sent? And so on.</p>
<p>&#8220;Solution&#8221;: The SQL language supplies a variety of COBOL-like functions to calculate whatever we can ask for. And also an opportunity to get things horribly wrong, because the SQL statement became way too complicated.</p>
<h2>Use POSIX time() instead</h2>
<p>Sorry, didn&#8217;t mean to scare you off. It&#8217;s really simple: Any modern operating system, even Windows, will readily supply you with the number of seconds since January 1, 1970, midnight, UTC (that is, more or less GMT). This is also called &#8220;seconds since the Epoch&#8221; or &#8220;UNIX time&#8221;.</p>
<p>No matter where the computer is, what timezone it uses or what programming language you&#8217;re using, this simple integer representation will show the same number at any given moment.</p>
<p>You can, in fact, obtain this number from MySQL directly:</p>
<pre>mysql&gt; SELECT UNIX_TIMESTAMP(thedate) FROM stupid_date;
+-------------------------+
| UNIX_TIMESTAMP(thedate) |
+-------------------------+
|              1237118503 |
+-------------------------+
1 row in set (0.00 sec)</pre>
<p>This means, that 1237118503 seconds elapsed since the Epoch (which is a global time point) until 14:01:43 in Israeli <span style="text-decoration: underline;">LOCAL time </span>of the day I wrote this post. So now we have an integer number to work with, which is handy for calculations, but things will still get messy if we try to move the database to another server.</p>
<h2>Store the number instead</h2>
<p>If we are interested in working with integers, why not store the integer itself in the database? We could go:</p>
<pre>mysql&gt; CREATE TABLE smart_date ( thedate INTEGER UNSIGNED, PRIMARY KEY (thedate) );
Query OK, 0 rows affected (0.00 sec)

mysql&gt; INSERT INTO smart_date(thedate) VALUES (1237118503);
Query OK, 1 row affected (0.00 sec)

mysql&gt; SELECT * FROM smart_date;
+------------+
| thedate    |
+------------+
| 1237118503 |
+------------+
1 row in set (0.00 sec)</pre>
<p>That wasn&#8217;t very impressive, was it? The first question would be &#8220;OK, how do I get this magic number, now that I don&#8217;t have the NOW() function?&#8221;</p>
<p>The short and not-so-clever answer is that you could always use MySQL&#8217;s UNIX_TIMESTAMP( NOW() ) for this. The better answer is that no matter which scripting or programming language you&#8217;re using, this number is very easy to obtain. I&#8217;ll show examples below.</p>
<p>As for the magnitude of this number, yes, it&#8217;s pretty big. But it will fit a signed 32-bit integer until year 2038. I presume that nobody will use 32-bit integers by then.</p>
<p>And finally, one could argue that DATETIME is convenient when reading from the database directly. True. But for that specific issue we have the FROM_UNIXTIME() function:</p>
<pre>mysql&gt; SELECT FROM_UNIXTIME(thedate) FROM smart_date;
+------------------------+
| FROM_UNIXTIME(thedate) |
+------------------------+
| 2009-03-15 14:01:43    |
+------------------------+
1 row in set (0.00 sec)</pre>
<p>And again, this is given in the computer&#8217;s local time. Which is fine, because it&#8217;s intended to be read by humans. In particular, humans who easily translate time differences between their server and themselves.</p>
<h2>Obtaining Epoch time</h2>
<p>Just to prove that it&#8217;s easy to know what the &#8220;Epoch time&#8221; is in any language, here are a few examples. Wherever it&#8217;s really simple, I&#8217;m showing how to convert this format to human-readable format.</p>
<p>In Perl:</p>
<pre>print time();
print scalar localtime time(); # Local time for humans</pre>
<p>In PHP:</p>
<pre>&lt;?php
echo time();
echo date('r', time() ); // Local time for humans
?&gt;</pre>
<p>In Python:</p>
<pre>from time import time;
print time();</pre>
<p>(note that the time is returned as a float number with higher precision)</p>
<p>In C:</p>
<pre>#include &lt;time.h&gt;
#include &lt;stdio.h&gt;

int main () {
  int now = time(NULL);

  printf("%d seconds since the Epoch\n", now);
  return 0;
}</pre>
<p>In JavaScript:</p>
<pre>&lt;script language="JavaScript" type="text/javascript"&gt;
now = new Date();
alert( now.getTime() / 1000 );
&lt;/script&gt;</pre>
<p>In this case, the time is shown with a fractional resolution.</p>
<p>The JavaScript example is not really useful for a database application, because the time is measured at the computer showing the page. In a website application, this is just anybody&#8217;s computer clock, which may be wrong. But it&#8217;s yet another example of how this time representation is widely available.</p>
<h2>Conclusion</h2>
<p>Drop those DATETIME columns from your tables, and use a simple, robust and handy format to represent time. Don&#8217;t let the database play around with a sensitive issue like time, and don&#8217;t risk getting confused by different functions when calculating time differences. Just because the DATETIME column type exists, it doesn&#8217;t mean there is a reason to use it.</p>
<p>Enjoy the database on what it&#8217;s best at: Storing and collecting information.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2009/03/mysql-datetime-epoch-unix-time/feed/</wfw:commentRss>
		<slash:comments>93</slash:comments>
		</item>
		<item>
		<title>BLOB, TEXT, and case sensitivity: MySQL won&#8217;t treat them the same</title>
		<link>https://billauer.se/blog/2009/03/mysql-blob-text-case-sensitivity/</link>
		<comments>https://billauer.se/blog/2009/03/mysql-blob-text-case-sensitivity/#comments</comments>
		<pubDate>Mon, 02 Mar 2009 09:59:51 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Software]]></category>
		<category><![CDATA[mysql blob text case sensitivity white spaces session select insert unique]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=13</guid>
		<description><![CDATA[When I first discovered that there is both BLOB and TEXT in databases, I was puzzled. They occupy the same amount of disk space, and the database doesn&#8217;t alter the data itself anyhow. Why two? Of course, I followed the stream, and went for TEXT and VARCHAR for everything, since I don&#8217;t store binary data [...]]]></description>
			<content:encoded><![CDATA[<p>When I first discovered that there is both BLOB and TEXT in databases, I was puzzled. They occupy the same amount of disk space, and the database doesn&#8217;t alter the data itself anyhow. Why two?</p>
<p>Of course, I followed the stream, and went for TEXT and VARCHAR for everything, since I don&#8217;t store binary data in the database. That may not be the optimal choice in all cases.</p>
<p>It turns out, that MySQL goes a long way to &#8220;help&#8221; the user with string operations. In particular, when the table column is defined as TEXT, VARCHAR and their derivatives, the database will compare strings as they would be understood by humans. And if that definition sounds ambiguous to you, you&#8217;re in good company: Different versions of MySQL compare text strings differently. For example, snipping leading and trailing whitespaces from the strings before comparing them: Some versions will do this, others won&#8217;t.</p>
<p>The bottom line is that if you change your MySQL server, tiny bugs may creep in. All these corner cases may behave differently. This is the classic case of some entry disappearing from a list of 53478, without anyone noticing.</p>
<p>Another issue to consider is character collation: When using TEXT, VARCHAR and friends, we also have that ‘Ö’ and ‘OE’ are treated as the same character (when German character set is used, among others). Just an example.</p>
<p>The solution: Make MySQL treat your data as a binary. A BLOB column type, for example. You loose all those extra &#8220;features&#8221;, but gain stability over database versions and flavors. That means that you have to handle all the case-insensitivity issues yourself, as well cleaning up the strings properly. With good practices, that&#8217;s not an issue. It&#8217;s a matter of if you want to take responsibility, or let the database iron those small wrinkles for you.</p>
<p>And finally: What about uniqueness in tables? Here&#8217;s a short session, using MySQL 4.0.24:</p>
<pre>mysql&gt; CREATE TABLE try (mydata TEXT, UNIQUE INDEX (mydata(20)) );
Query OK, 0 rows affected (0.00 sec)

mysql&gt; INSERT INTO try(mydata) VALUES('Hello');
Query OK, 1 row affected (0.00 sec)

mysql&gt; SELECT * FROM try WHERE mydata='hELLO';
+--------+
| mydata |
+--------+
| Hello  |
+--------+
1 row in set (0.00 sec)</pre>
<p>This was pretty much expected: As a TEXT column, the comparison was case-insensitive. And of course, the capital &#8220;H&#8221; was saved in the table, even though that doesn&#8217;t matter in string comparisons.</p>
<p>But what happens if we want to add an entry, which violates the uniqueness, when considering the strings in a case-insensitive manner?</p>
<pre>mysql&gt; INSERT INTO try(mydata) VALUES('HELLO');
ERROR 1062: Duplicate entry 'HELLO' for key 1</pre>
<p>As expected, MySQL didn&#8217;t swallow this. &#8220;Hello&#8221; and &#8220;HELLO&#8221; are the same, so they can&#8217;t live together when &#8220;mydata&#8221; is restricted as UNIQUE.</p>
<p>So let&#8217;s drop the table, and try this again, this time with a BLOB column. Spoiler: Everything is case-sensitive now.</p>
<pre>mysql&gt; DROP TABLE try;
Query OK, 0 rows affected (0.00 sec)

mysql&gt; CREATE TABLE try (mydata BLOB, UNIQUE INDEX (mydata(20)) );
Query OK, 0 rows affected (0.01 sec)

mysql&gt; INSERT INTO try(mydata) VALUES('Hello');
Query OK, 1 row affected (0.00 sec)

mysql&gt; SELECT * FROM try WHERE mydata='hELLO';
Empty set (0.00 sec)</pre>
<p>(why should it find anything? &#8216;Hello&#8217; and &#8216;hELLO&#8217; are completely different!)</p>
<pre>mysql&gt; SELECT * FROM try;
+--------+
| mydata |
+--------+
| Hello  |
+--------+
1 row in set (0.00 sec)

mysql&gt; INSERT INTO try(mydata) VALUES('HELLO');
Query OK, 1 row affected (0.00 sec)

mysql&gt; SELECT * FROM try;
+--------+
| mydata |
+--------+
| Hello  |
| HELLO  |
+--------+
2 rows in set (0.01 sec)</pre>
<p>(No problems with the uniqueness: &#8216;Hello&#8217; and &#8216;HELLO&#8217; are not the same in a BLOB)</p>
<p>To summarize all this: Before choosing between TEXT or BLOB, ask yourself if you want the database to treat the string exactly as it is, or if you want some forgiveness regarding case, whitespaces and natural language issues.</p>
<p>For example, are you sure that you want the user name and password as text? In particular, would you like the password case-insensitive? Do you want HTTP links as text? The address itself is indeed case-insensitive to the web, but CGI arguments (everything after the question mark, if present) <strong>is</strong> case-sensitive (YouTube video IDs, for example).</p>
<p>Usually, using a text column is OK. But it&#8217;s a choice one has to make.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2009/03/mysql-blob-text-case-sensitivity/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
	</channel>
</rss>
