Bug #102101 COUNT(*) returns null in nested query
Submitted: 31 Dec 2020 13:48 Modified: 11 Jan 2021 19:40
Reporter: Eduardo Ubide Email Updates:
Status: Closed Impact on me:
None 
Category:MySQL Server: Optimizer Severity:S3 (Non-critical)
Version:8.0.22 OS:Any
Assigned to: CPU Architecture:x86
Tags: regression

[31 Dec 2020 13:48] Eduardo Ubide
Description:
This query should return 0 if it finds nothing but returns a value of NULL. This only happens in version 8.0.22 and not in previous versions.

SELECT
  (SELECT count(*) +
     (SELECT count(*)
      FROM classified_linked
      WHERE companyId = 1
        AND classifiedId2 = c.classifiedId)
   FROM classified_linked
   WHERE companyId = 1
     AND classifiedId1 = c.classifiedId) AS linked
FROM
  (SELECT 555 AS classifiedId) AS c;

How to repeat:
Create an example table. 

CREATE TABLE `classified_linked` (
`companyId` int UNSIGNED NOT NULL,
`classifiedId1` int UNSIGNED NOT NULL,
`classifiedId2` int UNSIGNED NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

ALTER TABLE `classified_linked`
ADD PRIMARY KEY (`companyId`,`classifiedId1`,`classifiedId2`),
ADD KEY `classifiedId1` (`classifiedId1`),
ADD KEY `classifiedId2` (`classifiedId2`,`companyId`) USING BTREE;

The query that returns NULL:

SQL> SELECT
    ->   (SELECT count(*) +
    ->      (SELECT count(*)
    ->       FROM classified_linked
    ->       WHERE companyId = 1
    ->         AND classifiedId2 = c.classifiedId)
    ->    FROM classified_linked
    ->    WHERE companyId = 1
    ->      AND classifiedId1 = c.classifiedId) AS linked
    -> FROM
    ->   (SELECT 555 AS classifiedId) AS c;
+--------+
| linked |
+--------+
|   NULL |
+--------+
1 row in set (0.00 sec)

In version 8.0.21 and earlier this same query returns 0, which is the expected value.

Suggested fix:
Replacing innermost c.classifiedId reference with a value fixes it so the problem is related to:

SQL> SELECT
    ->   (SELECT count(*) +
    ->      (SELECT count(*)
    ->       FROM classified_linked
    ->       WHERE companyId = 1
    ->         AND classifiedId2 = 555)
    ->    FROM classified_linked
    ->    WHERE companyId = 1
    ->      AND classifiedId1 = c.classifiedId) AS linked
    -> FROM
    ->   (SELECT 555 AS classifiedId) AS c;
+--------+
| linked |
+--------+
|      0 |
+--------+
1 row in set (0.00 sec)

On the other hand, repeating the rewritten query of the explain warning, also returns the correct result.

SQL> EXPLAIN SELECT (SELECT count(*) + ...

WARNING : Note (Code 1003): /* select#1 */ select (/* select#2 */ select (count(0) + (/* select#3 */ select count(0) from `test`.`classified_linked` where ((`test`.`classified_linked`.`classifiedId2` = '555') and (`test`.`classified_linked`.`companyId` = 1)))) from `test`.`classified_linked` where ((`test`.`classified_linked`.`classifiedId1` = '555') and (`test`.`classified_linked`.`companyId` = 1))) AS `linked` from dual
[31 Dec 2020 13:58] MySQL Verification Team
Hello Eduardo Ubide,

Thank you for the report and test case.

regards,
Umesh
[11 Jan 2021 19:40] Jon Stephens
Fix documented in the MySQL 8.0.24 changelog, as follows:

    When the aggregate iterator finds no rows, it calls on each item
    in its SELECT list to inform them of this (for example, so that
    COUNT(*) can set itself to zero, or SUM(foo) can set itself to
    NULL). After internal work done in MySQL 8.0.22, it
    could also inadvertently call hidden items. In some queries with
    doubly nested subqueries, one such hidden item could become its
    own parent subquery (and scalar subqueries in MySQL have special
    legacy handling of this call, for queries which are not 
    ONLY_FULL_GROUP_BY), causing the entire subquery to return NULL 
    when it should not have done so.

    This is fixed by making the call only on visible items, as in
    MySQL 8.0.21 and earlier.

Closed.