Uploaded image for project: 'MariaDB Server'
  1. MariaDB Server
  2. MDEV-40562

Creating a secondary index on an ENUM column changes the result of MIN() / MAX()

    XMLWordPrintable

Details

    • 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.

      Attachments

        Activity

          People

            psergei Sergei Petrunia
            Jacob Ding Jiahao Ding
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

            Dates

              Created:
              Updated:

              Git Integration

                Error rendering 'com.xiplink.jira.git.jira_git_plugin:git-issue-webpanel'. Please contact your Jira administrators.