Details
-
Bug
-
Status: Confirmed (View Workflow)
-
Major
-
Resolution: Unresolved
-
10.11, 11.4, 11.8, 12.3, 12.3.2
-
Unexpected results
Description
On a table with an `ENUM` column, `SELECT MIN(e), MAX(e) FROM t` returns one answer when the column has no secondary index and a *different* answer after a secondary index is created on it. The data is unchanged and the statement text is unchanged; only the presence of the index differs. `IGNORE INDEX` restores the first answer, `FORCE INDEX` restores the second.
*What this report is not about.* The MariaDB documentation for `MIN()` and `MAX()` states:
> Note that SET and ENUM fields are currently compared by their string value rather than their relative position in the set, so MAX() may produce a different highest result than ORDER BY DESC.
We are *not* disputing this rule, and we are not arguing that `MIN()`/`MAX()` on an `ENUM` ought to use the definition order. (MySQL Bug #45300 was closed as "not a bug" on exactly that documented-behaviour ground, so we want to be explicit that this report makes a different claim.)
The claim of this report is threefold:
1. The documented rule is stated *unconditionally*. It contains no qualification such as "unless a secondary index on the column is present" or "unless the MIN/MAX index optimization is chosen".
2. The actual behaviour is *conditional on plan choice*. When the MIN/MAX index optimization fires, the result follows the ENUM definition order (the index order); when it does not fire, the result follows the string order that the documentation prescribes. Consequently, creating or dropping an index changes the result of a query, which breaks physical data independence.
3. Independently of which order is "right", a *single statement contradicts itself*: with the index present, `MAX(e)` returns `'ab'` while `MAX(e) OVER ()` over the same table returns `'C'`. Both expressions denote "the largest value of `e` in this table". No reading of the documentation makes both correct simultaneously.
```sql
DROP DATABASE IF EXISTS bugrep_mariadb;
CREATE DATABASE bugrep_mariadb;
USE bugrep_mariadb;
– definition order: b=1, a=2, C=3, ab=4
– string order under utf8mb4_general_ci: a < ab < b < C
CREATE TABLE en (id INT PRIMARY KEY, e ENUM('b','a','C','ab'));
INSERT INTO en VALUES (1,'b'),(2,'a'),(3,'C'),(4,'ab'),(5,'a'),(6,'b');
– (1) no secondary index
SELECT 'no index' AS state, MIN(e) AS min_e, MAX(e) AS max_e FROM en;
CREATE INDEX ix_e ON en (e);
– (2) same data, same query, only a secondary index was added
SELECT 'index added' AS state, MIN(e) AS min_e, MAX(e) AS max_e FROM en;
– (3) same statement, index still present, access path disabled by hint
SELECT 'IGNORE INDEX' AS state, MIN(e) AS min_e, MAX(e) AS max_e FROM en IGNORE INDEX (ix_e);
– (4) self-contradiction inside a single statement (index present)
SELECT (SELECT MAX(e) FROM en) AS agg_max,
(SELECT DISTINCT MAX(e) OVER () FROM en LIMIT 1) AS window_max,
(SELECT MIN(e) FROM en) AS agg_min,
(SELECT DISTINCT MIN(e) OVER () FROM en LIMIT 1) AS window_min;
EXPLAIN SELECT MIN(e), MAX(e) FROM en;
```
Expected Result
Steps (1), (2) and (3) must all return the same pair, because they are the same query over the same data. Per the documented string-value rule that pair is `('a','C')`. In step (4), `agg_max` must equal `window_max` and `agg_min` must equal `window_min`.
Actual Result
Measured on MariaDB 12.3.2 (all output below is from the script above, run as-is):
```
--------------------
| state | min_e | max_e |
--------------------
| no index | a | C | <- string order, matches the documentation --------- ------------ |
| state | min_e | max_e |
-----------------------
| index added | b | ab | <- WRONG: definition order, same data, same query ------------ ------------- |
| state | min_e | max_e |
------------------------
| IGNORE INDEX | a | C | <- back to string order ------------- -------- |
| agg_max | window_max | agg_min | window_min |
------------------------------------+
| ab | C | b | a | <- WRONG: one statement, two answers -------- ``` |
Cross-Engine Comparison
All engines below were measured on the same machine with the same table definition and the same six rows, before and after `CREATE INDEX ix_e ON en(e)`.
| Engine | MIN/MAX without index | MIN/MAX with index | Index-dependent? | `MAX(e)` vs `MAX(e) OVER ()` |
| — | — | — | — | — |
| MariaDB 12.3.2 | `('a','C')` | `('b','ab')` | *yes* | `'ab'` vs `'C'` — *disagree* |
| MySQL 9.7.1 | `('a','C')` | `('b','ab')` | *yes* (same defect) | `'ab'` vs `'C'` — *disagree* |
| TiDB v8.5.7 | `('C','b')` | `('C','b')` | no | `'b'` vs `'b'` — agree |
| PostgreSQL 18.4 (native `ENUM` type) | `('b','ab')` | `('b','ab')` | no | `'ab'` vs `'ab'` — agree |
| DuckDB 1.5.5 (native `ENUM` type) | `('b','ab')` | `('b','ab')` | no | `'ab'` vs `'ab'` — agree |
The reference engines disagree with each other about which order `MIN`/`MAX` on an enumerated type should use (TiDB uses a binary string order, PostgreSQL and DuckDB use the definition order), but every one of them is *index-independent* and *internally consistent*. MariaDB and MySQL are the only two that change the answer when an index appears.