Constant-Folding Optimization in MySQL 8.0
TL;TR
In MySQL 8.0.16 the optimizer has improved again!
Comparisons of columns of numeric types with constant values are checked and folded or removed for invalid or out-of-rage values.
The goal is to speed up query execution.
The name of this article (Constant-Folding Optimization), named after this kind of optimization, is quite cryptic. Nevertheless the principle is simple and more important there is nothing to do from the user perspective.
What is “Constant-Folding Optimization” ?
From the MySQL Documentation :
Comparisons between constants and column values in which the constant value is out of range or of the wrong type with respect to the column type are now handled once during query optimization rather row-by-row than during execution.
From the MySQL Server Team Blog :
The goal is to speed up execution at the cost of a little more analysis at optimize time.
Always true and false comparisons are detected and eliminated.
In other cases, the type of the constant is adjusted to match that of the field if they are not the same, avoiding type conversion at execution time.
Clear enough?
One example is worth a thousand words, so let’s have a deeper look comparing the old behavior in MySQL 8.0.15 to the new one beginning with MySQL 8.0.16.
I’m using the optimized MySQL Server Docker images, created, maintained and supported by the MySQL team at Oracle.
Deployment of MySQL 8.0.15 & MySQL 8.0.16:
$ docker run --name=mysql_8.0.15 -e MYSQL_ROOT_PASSWORD=unsafe -d mysql/mysql-server:8.0.15
$ docker run --name=mysql_8.0.16 -e MYSQL_ROOT_PASSWORD=unsafe -d mysql/mysql-server:8.0.16
Note:
Obviously using a password on the command line interface can be insecure.
Please read the best practices of deploying MySQL on Linux with Docker.
Copy the test table dump file on 8.0.15 & 8.0.16:
$ docker cp ./testtbl.sql mysql_8.0.15:/tmp/testtbl.sql
$ docker cp ./testtbl.sql mysql_8.0.16:/tmp/testtbl.sql
Load the test table into 8.0.15 instance:
$ docker exec -it mysql_8.0.15 mysql -u root -p --prompt='mysql_8.0.15> '
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 31
Server version: 8.0.15 MySQL Community Server - GPL
Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql_8.0.15> SELECT VERSION();
+-----------+
| VERSION() |
+-----------+
| 8.0.15 |
+-----------+
mysql_8.0.15> CREATE SCHEMA test;
Query OK, 1 row affected (0.04 sec)
mysql_8.0.15> USE test
Database changed
mysql_8.0.15> source /tmp/testtbl.sql
... <snip> ...
Load the test table into 8.0.16 instance:
$ docker exec -it mysql_8.0.16 mysql -u root -p --prompt='mysql_8.0.16> '
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 12
Server version: 8.0.16 MySQL Community Server - GPL
Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql_8.0.16> SELECT VERSION();
+-----------+
| VERSION() |
+-----------+
| 8.0.16 |
+-----------+
mysql_8.0.16> CREATE SCHEMA test;
Query OK, 1 row affected (0.04 sec)
mysql_8.0.16> USE test
Database changed
mysql_8.0.16> source /tmp/testtbl.sql
... <snip> ...
Let’s see what we have loaded:
mysql_8.0.16> SHOW CREATE TABLE testtbl\G
*************************** 1. row ***************************
Table: testtbl
Create Table: CREATE TABLE `testtbl` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`val` varchar(36) NOT NULL,
`val2` varchar(36) DEFAULT NULL,
`val3` varchar(36) DEFAULT NULL,
`val4` varchar(36) DEFAULT NULL,
`num` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx2` (`val2`),
KEY `idx3` (`val3`),
KEY `idx4` (`val4`)
) ENGINE=InnoDB AUTO_INCREMENT=14220001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
mysql_8.0.16> SELECT COUNT(*) FROM testtbl;
+----------+
| COUNT(*) |
+----------+
| 5000000 |
+----------+
What is important for us here is the non indexed column – num :num
int(10) unsigned DEFAULT NULL
It contains only positive numbers:
mysql_8.0.16> SELECT min(num), max(num) FROM testtbl;
+----------+----------+
| min(num) | max(num) |
+----------+----------+
| 9130001 | 14130000 |
+----------+----------+
The old behavior
What happens if I looking for a negative number, let’s say -12345, on the column num ?
Remember that it contains only positive numbers and there is no index.
mysql_8.0.15> EXPLAIN SELECT * FROM testtbl WHERE num=-12345\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: testtbl
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 4820634
filtered: 10.00
Extra: Using where
According to the EXPLAIN plan, we have a full table scan. In a way that makes sense because there is no index on num.
However we know that there is no negative value, so there is certainly some room for improvements 🙂
Running the query:
mysql_8.0.15> SELECT * FROM testtbl WHERE num=-12345;
Empty set (2.77 sec)
Indeed the full table scan could be costly.
The current behavior – 8.0.16+
The Constant-Folding Optimization improves the execution of this type of queries.
The EXPLAIN plan for MySQL 8.0.16 is completely different:
mysql_8.0.16> EXPLAIN SELECT * FROM testtbl WHERE num=-12345\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: NULL
partitions: NULL
type: NULL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: NULL
filtered: NULL
Extra: Impossible WHERE
Did you notice the: Extra: Impossible WHERE
Looking for the negative value in a strictly positive column was processed at the optimize time!
So they are obviously a positive impact on the query execution time:
mysql_8.0.16> SELECT * FROM testtbl WHERE num=-12345;
Empty set (0.00 sec)
Yay!
In addition to the = operator, this optimization is currently possible for >, >=, <, <=, =, <>, != and <=> as well.
e.g.
mysql_8.0.16> EXPLAIN SELECT * FROM testtbl WHERE num > -42 AND num <= -1 \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: NULL
partitions: NULL
type: NULL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: NULL
filtered: NULL
Extra: Impossible WHERE
mysql_8.0.16> SELECT * FROM testtbl WHERE num > -42 AND num <= -1;
Empty set (0.00 sec)
Indexed column
As a side note, if your column is indexed the optimizer already have the relevant information, so before 8.0.16, no need of Constant-Folding Optimization, to have a fast query :).
mysql_8.0.15> CREATE INDEX idx_num ON testtbl(num);
Query OK, 0 rows affected (24.84 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql_8.0.15> EXPLAIN SELECT * FROM testtbl WHERE num = -12345\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: NULL
partitions: NULL
type: NULL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: NULL
filtered: NULL
Extra: no matching row in const table
1 row in set, 1 warning (0.00 sec)
mysql_8.0.15> SELECT * FROM testtbl WHERE num = -12345;
Empty set (0.00 sec)
References
- Constant-Folding Optimization
- WL#11935: Add folding of constants when compared to fields
- Deploying MySQL on Linux with Docker
- Optimized MySQL Server Docker images
Thanks for using MySQL!
Watch my videos on my YouTube channel and subscribe.
Thanks for using HeatWave & MySQL!
Cloud Solutions Architect at Oracle
MySQL Geek, author, blogger and speaker
I’m an insatiable hunger of learning.
—–
Blog: www.dasini.net/blog/en/
Twitter: https://twitter.com/freshdaz
SlideShare: www.slideshare.net/freshdaz
Youtube: https://www.youtube.com/channel/UC12TulyJsJZHoCmby3Nm3WQ
—–