The optimizer in MySQL 5.7 leverages generated columns. Generated columns will physically store data in two cases: Either the column is defined as STORED or you create an index on a virtual column. The optimizer will leverage such an index automatically if it encounters the same expression in a statement. Let's see an example:
mysql> DESC squares;
+-------+------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+------------------+------+-----+---------+-------+
| dx | int(10) unsigned | YES | | NULL | |
| dy | int(10) unsigned | YES | | NULL | |
+-------+------------------+------+-----+---------+-------+
2 rows in set (0.00 sec)
mysql> SELECT COUNT(*) FROM squares;
+----------+
| COUNT(*) |
+----------+
| 2097152 |
+----------+
1 row in set (0.77 sec)
We have a large table with 2 million rows. Selecting rows by the surface area of squares can hardly leverage an index on dx or dy:
mysql> EXPLAIN SELECT * FROM squares WHERE dx*dy=221\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: squares
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 2092860
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
Now let's add an index over a generated, virtual column that defines the area:
mysql> ALTER TABLE squares ADD COLUMN (area INT AS (dx*dy));
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> ALTER TABLE squares ADD INDEX (area);
Query OK, 0 rows affected (5.24 sec)
Records: 0 Duplicates: 0 Warnings: 0
Now we can run query again:
mysql> EXPLAIN SELECT * FROM squares WHERE dx*dy=221\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: squares
partitions: NULL
type: ref
possible_keys: area
key: area
key_len: 5
ref: const
rows: 18682
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
I did not change the query! The WHERE condition is still dx*dy. Nevertheless the optimizer finds the generated column, sees the index and decides to leverage that.
So you can add complex indexes and without changing the application code you can benefit from these indexes. That makes life much easier.
One limitation though: It seems the optimizer recognizes expressions only in the WHERE clause. It will not use the generated column and index for the SELECT expression:
mysql> EXPLAIN SELECT SUM(dx*dy) FROM squares\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: squares
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 2092860
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
mysql> EXPLAIN SELECT SUM(area) FROM squares\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: squares
partitions: NULL
type: index
possible_keys: NULL
key: area
key_len: 5
ref: NULL
rows: 2092860
filtered: 100.00
Extra: Using index
1 row in set, 1 warning (0.00 sec)
Showing posts with label auto generated column. Show all posts
Showing posts with label auto generated column. Show all posts
Monday, April 4, 2016
Thursday, April 9, 2015
Secondary Indexes on XML BLOBs in MySQL 5.7
When storing XML documents in a BLOB or TEXT column there was no way to create indexes on individual XML elements or attributes. With the new auto generated columns in MySQL 5.7 (1st Release Candidate available now!) this has changed! Let me give you an example. Let's work on the following table:
The table has only two columns: docid and doc. Since MySQL 5.1 it is possible to extract the population value thanks to the XML functions like ExtractValue(...). But sorting the documents by the population of a country was impossible because population is not a dedicated column in the table. Starting with MySQL 5.7.6 DMR we can add an auto generated column that contains only the population. Let’s create that column:
The population value is extracted automatically from each document, stored in a dedicated column and the index is maintained. Really simple now. Note that the population value of the cities is NOT extracted.
What happens if we want to look for city names? Each document may contain several city names. First let’s extract the city names with the XML function and store it in an auto generated column again:
The XML function ExtractValue extracts the name attribute of all cities and concatenates these with whitespace. That makes it easy for us to leverage the FULLTEXT index in InnoDB:
All XML calculations are done automatically when storing data. Let’s add another XML document and query again:
Does this also work with JSON documents? There are JSON functions available in a labs release. These functions are currently implemented as user defined functions (UDF) in MySQL. UDFs are not supported in auto generated columns. So we have to wait until JSON functions are built-in to MySQL.
UPDATE: See this blogpost. There is a first labs release to use JSON functional indexes.
mysql> SELECT * FROM country\G
*************************** 1. row ***************************
docid: 1
doc: <country>
<name>Germany</name>
<population>82164700</population>
<surface>357022.00</surface>
<city name="Berlin"><population></population></city>
<city name="Frankfurt"><population>643821</population></city>
<city name="Hamburg"><population>1704735</population></city>
</country>
*************************** 2. row ***************************
docid: 2
doc: <country>
<name>France</name>
<surface></surface>
<city name="Paris"><population>445452</population></city>
<city name="Lyon"></city>
<city name="Brest"></city>
<population>59225700</population>
</country>
*************************** 3. row ***************************
docid: 3
doc: <country>
<population>10236000</population>
<name>Belarus</name>
<city name="Brest"><population></population></city>
</country>
*************************** 4. row ***************************
docid: 4
doc: <country>
<name>Pitcairn</name>
<population>52</population>
</country>
4 rows in set (0,00 sec)
The table has only two columns: docid and doc. Since MySQL 5.1 it is possible to extract the population value thanks to the XML functions like ExtractValue(...). But sorting the documents by the population of a country was impossible because population is not a dedicated column in the table. Starting with MySQL 5.7.6 DMR we can add an auto generated column that contains only the population. Let’s create that column:
mysql> ALTER TABLE country ADD COLUMN population INT UNSIGNED AS (CAST(ExtractValue(doc,"/country/population") AS UNSIGNED INTEGER)) STORED;
Query OK, 4 rows affected (0,21 sec)
Records: 4 Duplicates: 0 Warnings: 0
mysql> ALTER TABLE country ADD INDEX (population);
Query OK, 0 rows affected (0,22 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> SELECT docid FROM country ORDER BY population ASC;
+-------+
| docid |
+-------+
| 4 |
| 3 |
| 2 |
| 1 |
+-------+
4 rows in set (0,00 sec)
The population value is extracted automatically from each document, stored in a dedicated column and the index is maintained. Really simple now. Note that the population value of the cities is NOT extracted.
What happens if we want to look for city names? Each document may contain several city names. First let’s extract the city names with the XML function and store it in an auto generated column again:
mysql> ALTER TABLE country ADD COLUMN cities TEXT AS (ExtractValue(doc,"/country/city/@name")) STORED;
Query OK, 4 rows affected (0,62 sec)
Records: 4 Duplicates: 0 Warnings: 0
mysql> SELECT docid,cities FROM country;
+-------+--------------------------+
| docid | cities |
+-------+--------------------------+
| 1 | Berlin Frankfurt Hamburg |
| 2 | Paris Lyon Brest |
| 3 | Brest |
| 4 | |
+-------+--------------------------+
4 rows in set (0,01 sec)
The XML function ExtractValue extracts the name attribute of all cities and concatenates these with whitespace. That makes it easy for us to leverage the FULLTEXT index in InnoDB:
mysql> ALTER TABLE country ADD FULLTEXT (cities);
mysql> SELECT docid FROM country WHERE MATCH(cities) AGAINST ("Brest");
+-------+
| docid |
+-------+
| 2 |
| 3 |
+-------+
2 rows in set (0,01 sec)
All XML calculations are done automatically when storing data. Let’s add another XML document and query again:
mysql> INSERT INTO country (doc) VALUES ('<country><name>USA</name><city name="New York"/><population>278357000</population></country>');
Query OK, 1 row affected (0,00 sec)
mysql> SELECT * FROM country WHERE MATCH(cities) AGAINST ("New York");
+-------+----------------------------------------------------------------------------------------------+------------+----------+
| docid | doc | population | cities |
+-------+----------------------------------------------------------------------------------------------+------------+----------+
| 5 | <country><name>USA</name><city name="New York"/><population>278357000</population></country> | 278357000 | New York |
+-------+----------------------------------------------------------------------------------------------+------------+----------+
1 row in set (0,00 sec)
Does this also work with JSON documents? There are JSON functions available in a labs release. These functions are currently implemented as user defined functions (UDF) in MySQL. UDFs are not supported in auto generated columns. So we have to wait until JSON functions are built-in to MySQL.
UPDATE: See this blogpost. There is a first labs release to use JSON functional indexes.
What did we learn? tl;dr
With MySQL 5.7.6 it is possible to automatically create columns from XML elements or attributes and maintain indexes on this data. Search is optimized, MySQL is doing all the work for you. And Brest is not only in France but also a city in Belarus.
Labels:
auto generated column,
MySQL 5.7,
secondary index,
XML
Friday, March 13, 2015
Auto Generated Columns in MySQL 5.7: Two Indexes on one Column made easy
One of my customers wants to search for names in a table. But sometimes the search is case insensitive, next time search should be done case sensitive. The index on that column always is created with the collation of the column. And if you search with a different collation in mind, you end up with a full table scan. Here is an example:
The collation of the column `Name` is utf8_bin, so case sensitive. Let's search for a City:
Very efficient statement, using the index. But unfortunately it did not find the row as the search is based on the case sensitive collation.
Now let's change the collation for the WHERE clause:
The result is what we wanted but the query creates a full table scan. Not good. BTW: The warnings point you to the fact that the index could not be used.
"AS (Name) STORED" is the new stuff: In the brackets is the expression to calculate the column value. Here it is a simple copy of the Name column. The keyword STORED means that the data is physically stored and not calculated on the fly. This is necessary to create the index now:
As utf8_general_ci is the default collation with utf8, there is no need to specify this with the new column. Now let's see how to search:
Now we can search case sensitive (...WHERE Name=...) and case insensitive (WHERE Name_ci=...) and leverage indexes in both cases.
The problem
mysql> SHOW CREATE TABLE City\G
*************************** 1. row ***************************
Table: City
Create Table: CREATE TABLE `City` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`Name` char(35) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL,
`CountryCode` char(3) NOT NULL DEFAULT '',
`District` char(20) NOT NULL DEFAULT '',
`Population` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`ID`),
KEY `CountryCode` (`CountryCode`),
KEY `Name` (`Name`),
) ENGINE=InnoDB AUTO_INCREMENT=4080 DEFAULT CHARSET=latin1
1 row in set (0,00 sec)
The collation of the column `Name` is utf8_bin, so case sensitive. Let's search for a City:
mysql> SELECT Name,Population FROM City WHERE Name='berlin';
Empty set (0,00 sec)
mysql> EXPLAIN SELECT Name,Population FROM City WHERE Name='berlin';
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
| 1 | SIMPLE | City | NULL | ref | Name | Name | 106 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0,00 sec)
Very efficient statement, using the index. But unfortunately it did not find the row as the search is based on the case sensitive collation.
Now let's change the collation for the WHERE clause:
mysql> SELECT Name,Population FROM City WHERE Name='berlin' COLLATE utf8_general_ci;
+--------+------------+
| Name | Population |
+--------+------------+
| Berlin | 3386667 |
+--------+------------+
1 row in set (0,00 sec)
mysql> EXPLAIN SELECT Name,Population FROM City WHERE Name='berlin' COLLATE utf8_general_ci;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | City | NULL | ALL | Name | NULL | NULL | NULL | 4108 | 10.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 3 warnings (0,00 sec)
The result is what we wanted but the query creates a full table scan. Not good. BTW: The warnings point you to the fact that the index could not be used.
The solution
Now let's see how auto generated columns in the new MySQL 5.7 Development Milestone Release can help us. First let's create a copy of the Name column but with a different collation: mysql> ALTER TABLE City ADD COLUMN Name_ci char(35) CHARACTER SET utf8 AS (Name) STORED;
Query OK, 4079 rows affected (0,50 sec)
Records: 4079 Duplicates: 0 Warnings: 0
"AS (Name) STORED" is the new stuff: In the brackets is the expression to calculate the column value. Here it is a simple copy of the Name column. The keyword STORED means that the data is physically stored and not calculated on the fly. This is necessary to create the index now:
mysql> ALTER TABLE City ADD INDEX (Name_ci);
Query OK, 0 rows affected (0,13 sec)
Records: 0 Duplicates: 0 Warnings: 0
As utf8_general_ci is the default collation with utf8, there is no need to specify this with the new column. Now let's see how to search:
mysql> SELECT Name, Population FROM City WHERE Name_ci='berlin';
+--------+------------+
| Name | Population |
+--------+------------+
| Berlin | 3386667 |
+--------+------------+
1 row in set (0,00 sec)
mysql> EXPLAIN SELECT Name, Population FROM City WHERE Name_ci='berlin';nbsp;
+----+-------------+-------+------------+------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | City | NULL | ref | Name_ci | Name_ci | 106 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0,00 sec)
Now we can search case sensitive (...WHERE Name=...) and case insensitive (WHERE Name_ci=...) and leverage indexes in both cases.
tl;dr
Use auto generated columns in MySQL 5.7 to create an additional index with a different collation. Now you can search based on different indexes.
Subscribe to:
Posts (Atom)