continuing the above analysis
explain format=json SELECT t1.c0 as t1c0, t0.c0 as t0c0 FROM t1 JOIN t0 ON t1.c0 >= t0.c0;
|
we see that the query plan is collect rows from t0 with data from t1 via a range-checked-for-each-record algorithm.
{
|
"query_block": {
|
"select_id": 1,
|
"cost": 0.02187429,
|
"nested_loop": [
|
{
|
"table": {
|
"table_name": "t1",
|
"access_type": "ALL",
|
"loops": 1,
|
"rows": 2,
|
"cost": 0.01034841,
|
"filtered": 100
|
}
|
},
|
{
|
"range-checked-for-each-record": {
|
"keys": ["i1"],
|
"table": {
|
"table_name": "t0",
|
"access_type": "ALL",
|
"possible_keys": ["i1"],
|
"loops": 2,
|
"rows": 4,
|
"cost": 0.01152588,
|
"filtered": 100
|
}
|
}
|
}
|
]
|
}
|
}
|
During the SQL_SELECT::test_quick_select call within JOIN::exec ... evaluate_join_record we construct a SEL_TREE on the Item representing the join condition t1.c0 > t0.c0.
The first row from t1 is this
As this is a TEXT field, the Field object is a Field_blob and a call to Field_blob::get_key_image_itRAW() is meant to write into a passed in buffer a representation of the contents of this blob ("19").
Here we see some additional checks on the actual length of this field taking into account the character set
size_t local_char_length= length / mbmaxlen();
|
local_char_length= field_charset()->charpos(blob, blob + blob_length,
|
local_char_length);
|
set_if_smaller(blob_length, local_char_length);
|
the field length is 3, this is the key length as defined by the index
CREATE INDEX i1 ON t0(c0(1) ASC);
|
if we use a full length index, we do not see this problem.
mbmaxlen() is 3, so local_char_length is initialized with the value of 1.
The next line we search for the last character in the string, in a charset aware way, and return the number of characters before this character.
This is again 1, searching for '9' in the string "19" and seeing only '1' prior to it.
So local_char_length is calculated as 1, the buffer is filled like this
(gdb) x/4cb buff
|
0x7fa68420e219: 1 '\001' 0 '\000' 49 '1' 0 '\000'
|
instead of what we would expect
{1, null, '1', '9', null}
with a full length index.
This buffer is then used as the key value to construct the SEL_TREE.
Using the collation utf8mb4_unicode_ci, we can observe that while "19'"is greater than the first value from t0 we are to compare it with
MariaDB [test]> select cast('19' as char character set utf8mb3 collate utf8mb3_unicode_ci) > cast('㏫' as char character set utf8mb3 collate utf8mb3_unicode_ci);
|
+--------------------------------------------------------------------------------------------------------------------------------------------+
|
| cast('19' as char character set utf8mb3 collate utf8mb3_unicode_ci) > cast('㏫' as char character set utf8mb3 collate utf8mb3_unicode_ci) |
|
+--------------------------------------------------------------------------------------------------------------------------------------------+
|
| 1 |
|
+--------------------------------------------------------------------------------------------------------------------------------------------+
|
1 row in set (0.003 sec)
|
if we truncate "19" to just "1"
MariaDB [test]> select cast('1' as char character set utf8mb3 collate utf8mb3_unicode_ci) > cast('㏫' as char character set utf8mb3 collate utf8mb3_unicode_ci);
|
+-------------------------------------------------------------------------------------------------------------------------------------------+
|
| cast('1' as char character set utf8mb3 collate utf8mb3_unicode_ci) > cast('㏫' as char character set utf8mb3 collate utf8mb3_unicode_ci) |
|
+-------------------------------------------------------------------------------------------------------------------------------------------+
|
| 0 |
|
+-------------------------------------------------------------------------------------------------------------------------------------------+
|
1 row in set (0.003 sec)
|
we see the "1" is NOT greater, so the range scan indicates that multi_range_read_next(), part of the range-checked-for-each-record algorithm should skip over this value during join execution. As we are executing a LEFT join, we reject the row from t0 as not matching t1 and discard this row.
I'm concluding that, because of the nature of the index and the collation specified, this is not a bug, but an expected behaviour.
Verified as described:
+------+------+------+
| c0 | c0 | cond |
+------+------+------+
| 19 | ㏫ | 1 |
| 6 | ㏫ | 1 |
| 19 | 61 | 0 |
| 6 | 61 | 0 |
| 19 | 29 | 0 |
| 6 | 29 | 1 |
| 19 | 99 | 0 |
| 6 | 99 | 0 |
+------+------+------+
Looks good so far.
Not I run an INNER join with IGNORE INDEX (i1):
SELECT ALL t1.c0 AS ref0 FROM t1 INNER JOIN t0 IGNORE INDEX (i1) ON t1.c0 >= t0.c0;
+------+
| ref0 |
+------+
| 19 |
| 6 |
| 6 |
+------+
Looks good so far again. It returned all rows which had cond=1 in the previous query.
Now I run the same INNER joins wihtout the IGNORE:
SELECT ALL t1.c0 AS ref0 FROM t1 INNER JOIN t0 ON t1.c0 >= t0.c0;
+------+
| ref0 |
+------+
| 6 |
| 6 |
+------+
Looks wrong. The record 19 disappeared.
The problem is also repeatable with utf8mb4_unicode_ci.