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

LIMIT / OFFSET is discarded when a table-less scalar subquery is flattened, so a zero-row subquery returns a value instead of NULL

    XMLWordPrintable

Details

    • Unexpected results
    • Q1/2027 Server Maintenance

    Description

      Description

      A scalar subquery that produces zero rows must evaluate to NULL. A table-less query such as `SELECT 1` produces exactly one row, but `LIMIT 0` or `OFFSET 1` truncates it to zero rows, so `(SELECT 1 LIMIT 1 OFFSET 1)` must be NULL.

      MariaDB removes the table-less subquery during optimization and replaces it with its select-list expression, discarding the `LIMIT` / `OFFSET` clause. `SELECT (SELECT 1 LIMIT 1 OFFSET 1)` therefore returns `1`.

      The defect is confined to the table-less flattening path. Every other way of producing zero rows in a scalar subquery is handled correctly by MariaDB, including the same `LIMIT`/`OFFSET` clauses on a subquery that has a `FROM` clause. This is what makes the diagnosis concrete: the "this query is guaranteed to return exactly one row" assumption that licenses the flattening is not re-checked against `LIMIT` / `OFFSET`.

      Minimal Reproduction

      DROP DATABASE IF EXISTS bugrep_mariadb;
      CREATE DATABASE bugrep_mariadb;
      USE bugrep_mariadb;
       
      SELECT 'SELECT (SELECT 1 LIMIT 1 OFFSET 1)'  AS form, (SELECT 1 LIMIT 1 OFFSET 1)  AS r;
      SELECT 'SELECT (SELECT 1 LIMIT 0)'           AS form, (SELECT 1 LIMIT 0)           AS r;
      SELECT 'SELECT (SELECT 1 LIMIT 10 OFFSET 5)' AS form, (SELECT 1 LIMIT 10 OFFSET 5) AS r;
      

      -- the wrong value propagates into surrounding expressions
      SELECT '... IS NULL' AS form, (SELECT 1 LIMIT 1 OFFSET 1) IS NULL AS r;
      SELECT '... + 100'   AS form, (SELECT 1 LIMIT 1 OFFSET 1) + 100   AS r;
      
      

      -- controls: every other way of producing zero rows is handled correctly
      CREATE TABLE one_row (x INT);
      INSERT INTO one_row VALUES (1234);
      SELECT 'with FROM, OFFSET 1'        AS form, (SELECT 1 FROM one_row LIMIT 1 OFFSET 1)           AS r;
      SELECT 'with FROM, LIMIT 0'         AS form, (SELECT x FROM one_row LIMIT 0)                    AS r;
      SELECT 'with FROM, WHERE 1=0'       AS form, (SELECT 1 FROM one_row WHERE 1=0)                  AS r;
      SELECT 'aggregate + false HAVING'   AS form, (SELECT COUNT(*) FROM one_row HAVING COUNT(*)>100) AS r;
      SELECT 'table-less + false HAVING'  AS form, (SELECT 1 AS c HAVING 1=0)                         AS r;
      SELECT 'table-less + false WHERE'   AS form, (SELECT 1 WHERE 1=0)                               AS r;
      SELECT 'table-less OFFSET, EXISTS'  AS form, EXISTS (SELECT 1 LIMIT 1 OFFSET 1)                 AS r;
      SELECT 'table-less OFFSET, derived' AS form,
             (SELECT COUNT(*) FROM (SELECT 1 AS x LIMIT 1 OFFSET 1) d)                                AS r;
       
      EXPLAIN EXTENDED SELECT (SELECT 1 LIMIT 1 OFFSET 1) AS r;
      SHOW WARNINGS;
      

      Expected Result

      The first three queries return `NULL`; `IS NULL` returns `1`; `... + 100` returns `NULL`.

      Actual Result

      Measured on MariaDB 12.3.2:

      ```

      SELECT (SELECT 1 LIMIT 1 OFFSET 1) 1
          • WRONG, expected NULL
      SELECT (SELECT 1 LIMIT 0) 1
          • WRONG, expected NULL
      SELECT (SELECT 1 LIMIT 10 OFFSET 5) 1
          • WRONG, expected NULL
      ... IS NULL 0
          • WRONG, expected 1
      ... + 100 101
          • WRONG, expected NULL

      controls, all correct:

      with FROM, OFFSET 1 NULL
      with FROM, LIMIT 0 NULL
      with FROM, WHERE 1=0 NULL
      aggregate + false HAVING NULL
      table-less + false HAVING NULL
      table-less + false WHERE NULL
      table-less OFFSET, EXISTS 0
      table-less OFFSET, derived 0

      ```

      The last two controls are worth noting: the same table-less `LIMIT 1 OFFSET 1` subquery is treated as zero rows when it appears under `EXISTS` or as a derived table. Only the scalar-subquery-in-projection form is wrong, which localises the defect precisely.

      (`1 IN (SELECT 1 LIMIT 1 OFFSET 1)` cannot be used as a further probe: MariaDB rejects it with `ERROR 1235 ... 'LIMIT & IN/ALL/ANY/SOME subquery'`.)

      Cross-Engine Comparison

      Same three statements on the same machine:

      Engine `(SELECT 1 LIMIT 1 OFFSET 1)` `(SELECT 1 LIMIT 0)` `(SELECT 1 LIMIT 10 OFFSET 5)`
      — — — —
      *MariaDB 12.3.2* *1* *1* *1*
      MySQL 9.7.1 NULL NULL NULL
      TiDB v8.5.7 NULL NULL NULL
      PostgreSQL 18.4 NULL NULL NULL
      DuckDB 1.5.5 NULL NULL NULL

      MariaDB is the only engine of the five that returns a value here.

      Plan / Activation Evidence

      EXPLAIN EXTENDED SELECT (SELECT 1 LIMIT 1 OFFSET 1) AS r;
       
      +------+-------------+-------+------+---------------+------+---------+------+------+----------+----------------+
      | id   | select_type | table | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra          |
      +------+-------------+-------+------+---------------+------+---------+------+------+----------+----------------+
      |    1 | SIMPLE      | NULL  | NULL | NULL          | NULL | NULL    | NULL | NULL |     NULL | No tables used |
      +------+-------------+-------+------+---------------+------+---------+------+------+----------+----------------+
      

      SHOW WARNINGS;
      +-------+------+------------------------------------------+
      | Level | Code | Message                                  |
      +-------+------+------------------------------------------+
      | Note  | 1249 | Select 2 was reduced during optimization |
      | Note  | 1003 | select 1 AS `r`                          |
      +-------+------+------------------------------------------+
      

      This is the whole bug in two lines: the subquery is eliminated (`Select 2 was reduced during optimization`) and the rewritten statement shown by the optimizer is literally `select 1 AS r` — the `LIMIT 1 OFFSET 1` has vanished from the plan.

      For contrast, the same subquery with a `FROM` clause is kept as a real subquery and returns NULL:

      |    1 | PRIMARY     | NULL    | NULL | ... | No tables used |
      |    2 | SUBQUERY    | one_row | ALL  | ... |                |
      

      Root Cause Analysis

      Hypothesis. The optimization that removes a table-less query block ("select ... was reduced during optimization") is licensed by the assumption that such a block returns exactly one row, so a scalar subquery over it can be replaced by its select-list expression. That assumption is invalid when the block carries a row-limiting clause: `LIMIT 0` and any non-zero `OFFSET` can reduce the guaranteed single row to zero rows, at which point the scalar subquery must evaluate to NULL. The reduction appears not to test for the presence of `LIMIT` / `OFFSET` before firing. MariaDB does perform this test for `WHERE 1=0` and for `HAVING 1=0` on the same table-less block (both controls return NULL correctly), so the missing case is specifically the row-limiting clauses.

      Upstream Reference

      This is the same defect that MySQL fixed:

      • *MySQL Bug#37293822* — "Wrong result with OFFSET in table-less subquery", fixed by commit [101f0aed74](https://github.com/mysql/mysql-server/commit/101f0aed74) (commit title verified: `Bug#37293822: Wrong result with OFFSET in table-less subquery`). MySQL 9.7.1 on this machine returns NULL for all three forms.
      • A closely related sibling in the same family is *MySQL Bug#37293786* — "Wrong result with QUALIFY in table-less query", commit [57bf7f9708](https://github.com/mysql/mysql-server/commit/57bf7f9708) (title verified) — the same guaranteed-one-row assumption defeated by a different zero-row-truncating clause. MariaDB has no `QUALIFY`, so only the `LIMIT` / `OFFSET` form applies here.

      Fixed-status contrast measured on this machine:

      Engine Status for `SELECT (SELECT 1 LIMIT 1 OFFSET 1)`
      — —
      MySQL fixed (Bug#37293822 / commit 101f0aed74); 9.7.1 returns NULL
      TiDB v8.5.7 not affected
      PostgreSQL 18.4 not affected
      DuckDB 1.5.5 not affected
      *MariaDB 12.3.2* *affected*

      Workaround

      Give the subquery a `FROM` clause, or express the zero-row condition another way — `(SELECT 1 FROM one_row LIMIT 1 OFFSET 1)`, `(SELECT 1 WHERE 1=0)` and `(SELECT 1 AS c HAVING 1=0)` all return NULL correctly. There is no configuration-level workaround: the reduction fires in the default configuration.

      Environment

      ```
      Server: MariaDB 12.3.2-MariaDB (Homebrew), 127.0.0.1:3307
      OS: macOS (darwin 25.5.0, arm64)
      sql_mode: STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
      optimizer_switch: server default, unmodified
      ```

      Attachments

        Issue Links

          Activity

            People

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

              Dates

                Created:
                Updated:

                Time Tracking

                  Estimated:
                  Original Estimate - 0d
                  0d
                  Remaining:
                  Time Spent - 2.1d Remaining Estimate - 1.5h
                  1.5h
                  Logged:
                  Time Spent - 2.1d Remaining Estimate - 1.5h
                  2.1d

                  Git Integration

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