Bug #120429 IFNULL function does not handle NULL
Submitted: 10 May 7:04 Modified: 11 May 9:03
Reporter: Tomas Valinčius Email Updates:
Status: Verified Impact on me:
None 
Category:MySQL Server: Optimizer Severity:S2 (Serious)
Version:9.7.0 OS:Windows (Windows Server 2022)
Assigned to: CPU Architecture:Any
Tags: IFNULL()

[10 May 7:04] Tomas Valinčius
Description:
MySQL 9.7 release has important change to IFNULL() and that broke functionality in some cases.

See Release notes, page 28:
https://docs.oracle.com/cd/E17952_01/mysql-9.7-relnotes-en/mysql-9.7-relnotes-en.pdf

All functions below should return identical value:
(1)  IFNULL(t.Volume, t.VolumeNorm) AS Value_IfNull
(2)  COALESCE(t.Volume, t.VolumeNorm) AS Value_Coalesce
(3)  IF(t.Volume IS NULL, t.VolumeNorm, t.Volume) AS Value_If

The variant (1) with IFNULL function fails.

How to repeat:
-----
1. Create sample database
-----

DROP DATABASE IF EXISTS mysqlbug;
CREATE DATABASE mysqlbug CHARACTER SET utf8mb4 COLLATE utf8mb4_lithuanian_ci;
USE mysqlbug;

DROP TABLE IF EXISTS OrderServiceComposition;
DROP TABLE IF EXISTS OrderServices;
DROP TABLE IF EXISTS ServicesComposition;
DROP TABLE IF EXISTS Services;

CREATE TABLE Services
(
    Id INT UNSIGNED NOT NULL AUTO_INCREMENT,
    Name VARCHAR(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_lithuanian_ci NOT NULL,
    DurationInMinutes MEDIUMINT UNSIGNED NULL, -- [0; 16777215] (3 bytes)
    BreakInMinutes MEDIUMINT UNSIGNED NULL, -- [0; 16777215] (3 bytes)
    Unit SMALLINT UNSIGNED NULL, -- Enum value [0; 65535]
    Volume DECIMAL(9, 3) NULL CONSTRAINT CH_Services_Volume_Unsigned CHECK (Volume IS NULL OR Volume >= 0), -- [0; 999,999.999]
    HasComposition BOOL NOT NULL DEFAULT 0,

    CONSTRAINT PK_Services_Id PRIMARY KEY (Id),
    CONSTRAINT CH_Services_IsServiceOrMaterial CHECK (
        (DurationInMinutes IS NULL AND (Unit IS NOT NULL AND Volume IS NOT NULL AND BreakInMinutes IS NULL)) OR
        (DurationInMinutes IS NOT NULL AND (Unit IS NULL AND Volume IS NULL))
    )
);
INSERT INTO Services VALUES (2,'Name1',0,0,NULL,NULL,0);
INSERT INTO Services VALUES (15,'Name2',0,0,NULL,NULL,0);
INSERT INTO Services VALUES (19,'Name3',0,0,NULL,NULL,0);
INSERT INTO Services VALUES (27,'Name4',0,0,NULL,NULL,0);
INSERT INTO Services VALUES (126,'Name5',0,0,NULL,NULL,1);
INSERT INTO Services VALUES (127,'Name6',0,0,NULL,NULL,1);
INSERT INTO Services VALUES (162,'Name7',NULL,NULL,1,100.000,0);

CREATE TABLE ServicesComposition
(
    ServiceId INT UNSIGNED NOT NULL,

    CompServicePosition SMALLINT UNSIGNED NOT NULL,
    CompServiceId INT UNSIGNED NOT NULL,
    CompServiceCount DECIMAL(9, 3) NOT NULL CONSTRAINT CH_ServicesComposition_CompServiceCount_NotZero CHECK (CompServiceCount > 0),

    CONSTRAINT PK_ServicesComposition_ServiceId_CompServiceId PRIMARY KEY (ServiceId, CompServiceId),
    CONSTRAINT FK_ServicesComposition_ServiceId FOREIGN KEY (ServiceId) REFERENCES Services (Id),

    INDEX IX_ServicesComposition_CompServiceId (CompServiceId),
    CONSTRAINT FK_ServicesComposition_CompServiceId FOREIGN KEY (CompServiceId) REFERENCES Services (Id)
);
INSERT INTO ServicesComposition VALUES (126,1,19,1.000);
INSERT INTO ServicesComposition VALUES (126,2,27,1.000);
INSERT INTO ServicesComposition VALUES (126,3,162,1.000);
INSERT INTO ServicesComposition VALUES (127,1,15,1.000);
INSERT INTO ServicesComposition VALUES (127,2,126,1.000);

CREATE TABLE OrderServices
(
    Id BINARY(16) NOT NULL, -- Guid
    OrderId BINARY(16) NOT NULL, -- Guid
    Position SMALLINT UNSIGNED NOT NULL,

    ServiceId INT UNSIGNED NOT NULL,
    INDEX IX_OrderServices_ServiceId (ServiceId),
    CONSTRAINT FK_OrderServices_ServiceId FOREIGN KEY (ServiceId) REFERENCES Services (Id),

    CONSTRAINT PK_OrderServices_Id PRIMARY KEY (Id),
    INDEX IX_OrderServices_OrderId (OrderId, Position),

    DurationInMinutesNorm MEDIUMINT UNSIGNED NULL,
    BreakAfterInMinutesNorm MEDIUMINT UNSIGNED NULL,
    DurationInMinutes MEDIUMINT UNSIGNED NULL,
    Unit SMALLINT UNSIGNED NULL,
    Volume DECIMAL(9, 3) NULL CONSTRAINT CH_OrderServices_Volume_Unsigned CHECK (Volume IS NULL OR Volume >= 0), -- [0; 999,999.999]
    UnitPriceWithVat DECIMAL(14, 4) NOT NULL,

    CONSTRAINT CH_OrderServices_IsServiceOrMaterial CHECK (
        (DurationInMinutes IS NULL AND (Unit IS NOT NULL AND Volume IS NOT NULL AND DurationInMinutesNorm IS NULL)) OR
        (DurationInMinutes IS NOT NULL AND (Unit IS NULL AND Volume IS NULL AND DurationInMinutesNorm IS NOT NULL))
    )
);
INSERT INTO OrderServices VALUES (0x019E0EAFFA6A79F984E62B28024912D2,0x019E0EB00A8779C1B2BB2382A6AB05F3,1,126,  0,    0,  40,NULL,  NULL,    5.1971);
INSERT INTO OrderServices VALUES (0x019E0EAFFA6A7BEE9BB9998299F0ACB2,0x019E0EB00A8779C1B2BB2382A6AB05F3,0,  2,  0,    0,   0,NULL,  NULL,   -5.0112);
INSERT INTO OrderServices VALUES (0x019E0EAFFA6E76AB9A38509D7E797ECF,0x019E0EB00A8779C1B2BB2382A6AB05F3,3,162,NULL,NULL,NULL,   4,50.000,    0.9900);
INSERT INTO OrderServices VALUES (0x019E0EAFFA6E7820AC9967347FDDC632,0x019E0EB00A8779C1B2BB2382A6AB05F3,4,162,NULL,NULL,NULL,   4,50.000,    5.1781);
INSERT INTO OrderServices VALUES (0x019E0EAFFA6E78F1AADAC571E0001124,0x019E0EB00A8779C1B2BB2382A6AB05F3,2,126,   0,   0,  35,NULL,  NULL,    9.9900);

CREATE TABLE OrderServiceComposition
(
    OrderServiceId BINARY(16) NOT NULL, -- Guid
    OrderId BINARY(16) NOT NULL, -- Guid
    Position SMALLINT UNSIGNED NOT NULL,

    INDEX IX_OrderServiceComposition_OrderId (OrderId),
    -- CONSTRAINT FK_OrderServiceComposition_OrderId FOREIGN KEY (OrderId) REFERENCES Orders (Id),

    ServiceId INT UNSIGNED NOT NULL,
    INDEX IX_OrderServiceComposition_ServiceId (ServiceId),
    CONSTRAINT FK_OrderServiceComposition_ServiceId FOREIGN KEY (ServiceId) REFERENCES Services (Id),

    CONSTRAINT PK_OrderServiceComposition_OrderServiceId_ServiceId PRIMARY KEY (OrderServiceId, ServiceId),
    CONSTRAINT FK_OrderServiceComposition_OrderServiceId FOREIGN KEY (OrderServiceId) REFERENCES OrderServices (Id),

    DurationInMinutesNorm MEDIUMINT UNSIGNED NULL,
    BreakAfterInMinutesNorm MEDIUMINT UNSIGNED NULL,
    DurationInMinutes MEDIUMINT UNSIGNED NULL,
    UnitNorm SMALLINT UNSIGNED NULL,
    VolumeNorm DECIMAL(9, 3) NULL CONSTRAINT CH_OrderServiceComposition_VolumeNorm_Unsigned CHECK (VolumeNorm IS NULL OR VolumeNorm >= 0),
    Unit SMALLINT UNSIGNED NULL,
    Volume DECIMAL(9, 3) NULL CONSTRAINT CH_OrderServiceComposition_Volume_Unsigned CHECK (Volume IS NULL OR Volume >= 0),

    QuantityNorm DECIMAL(9, 3) NOT NULL CONSTRAINT CH_OrderServiceComposition_QuantityNorm_Unsigned CHECK (QuantityNorm >= 0),
    Quantity DECIMAL(9, 3) NOT NULL CONSTRAINT CH_OrderServiceComposition_Quantity_Unsigned CHECK (Quantity >= 0),
    QuantityTotal DECIMAL(9, 3) NOT NULL CONSTRAINT CH_OrderServiceComposition_QuantityTotal_Unsigned CHECK (QuantityTotal >= 0),

    CONSTRAINT CH_OrderServiceComposition_IsServiceOrMaterial CHECK (
        (DurationInMinutes IS NULL AND (Unit IS NOT NULL AND Volume IS NOT NULL)) OR
        (DurationInMinutes IS NOT NULL AND (Unit IS NULL AND Volume IS NULL))
    )
);
INSERT INTO OrderServiceComposition VALUES (0x019E0EAFFA6A79F984E62B28024912D2,0x019E0EB00A8779C1B2BB2382A6AB05F3,0,19,5,0,5,NULL,NULL,NULL,NULL,0.000,2.000,0.000);
INSERT INTO OrderServiceComposition VALUES (0x019E0EAFFA6A79F984E62B28024912D2,0x019E0EB00A8779C1B2BB2382A6AB05F3,0,162,NULL,NULL,NULL,5,50.000,5,50.000,0.000,1.500,0.000);
INSERT INTO OrderServiceComposition VALUES (0x019E0EAFFA6E78F1AADAC571E0001124,0x019E0EB00A8779C1B2BB2382A6AB05F3,0,19,5,0,5,NULL,NULL,NULL,NULL,0.000,2.000,0.000);
INSERT INTO OrderServiceComposition VALUES (0x019E0EAFFA6E78F1AADAC571E0001124,0x019E0EB00A8779C1B2BB2382A6AB05F3,0,27,0,0,0,NULL,NULL,NULL,NULL,0.000,1.000,0.000);

-----
2. Run the query below
-----
WITH normComposition AS (
SELECT
  os.Id AS OrderServiceId, sc.CompServiceId AS ServiceId,
  s.DurationInMinutes AS DurationInMinutesNorm,
  IFNULL(s.BreakInMinutes, IF(s.Unit IS NULL, 0, NULL)) AS BreakAfterInMinutesNorm,
  s.Unit AS UnitNorm, s.Volume AS VolumeNorm, sc.CompServiceCount AS QuantityNorm,
  sc.CompServicePosition, s.Name AS ServiceName, s.HasComposition
FROM OrderServices os
INNER JOIN ServicesComposition sc ON os.ServiceId = sc.ServiceId
INNER JOIN Services s ON sc.CompServiceId = s.Id
WHERE os.OrderId = uuid_to_bin('019e0eb0-0a87-79c1-b2bb-2382a6ab05f3')
  AND os.UnitPriceWithVat >= 0 AND os.ServiceId >= 10
),

savedComposition AS (
SELECT
  osc.OrderServiceId, osc.ServiceId, osc.DurationInMinutes, osc.Unit, osc.Volume, osc.Quantity, osc.QuantityTotal, osc.Position,
  s.DurationInMinutes AS DurationInMinutesNorm,
  IFNULL(s.BreakInMinutes, IF(s.Unit IS NULL, 0, NULL)) AS BreakAfterInMinutesNorm,
  s.Unit AS UnitNorm, s.Volume AS VolumeNorm, s.Name AS ServiceName, s.HasComposition
FROM OrderServiceComposition osc
INNER JOIN Services s ON osc.ServiceId = s.Id
WHERE osc.OrderId = uuid_to_bin('019e0eb0-0a87-79c1-b2bb-2382a6ab05f3')
),

fullJoin AS (
SELECT
  s.OrderServiceId, s.ServiceId,
  s.DurationInMinutes, s.Unit, s.Volume, s.Quantity, s.QuantityTotal,
  s.DurationInMinutesNorm, s.BreakAfterInMinutesNorm, s.UnitNorm, s.VolumeNorm, n.QuantityNorm,
  s.Position, n.CompServicePosition,
  s.ServiceName, s.HasComposition
FROM savedComposition s
LEFT JOIN normComposition n ON s.OrderServiceId = n.OrderServiceId AND s.ServiceId = n.ServiceId
UNION ALL
SELECT
  n.OrderServiceId, n.ServiceId,
  s.DurationInMinutes, s.Unit, s.Volume, s.Quantity, s.QuantityTotal,
  n.DurationInMinutesNorm, n.BreakAfterInMinutesNorm,
  n.UnitNorm, n.VolumeNorm, n.QuantityNorm,
  s.Position, n.CompServicePosition,
  n.ServiceName, n.HasComposition
FROM savedComposition s
RIGHT JOIN normComposition n ON s.OrderServiceId = n.OrderServiceId AND s.ServiceId = n.ServiceId
WHERE s.OrderServiceId IS NULL
)

SELECT
  bin_to_uuid(t.OrderServiceId),
  ROW_NUMBER() OVER (PARTITION BY t.OrderServiceId ORDER BY IFNULL(t.CompServicePosition, 999999), t.Position) AS Position,

  -- All values below must be equal, but IFNULL function works incorrect
  IFNULL(t.Volume, t.VolumeNorm) AS Value_IfNull,
  COALESCE(t.Volume, t.VolumeNorm) AS Value_Coalesce,
  IF(t.Volume IS NULL, t.VolumeNorm, t.Volume) AS Value_If
FROM fullJoin t
WHERE t.OrderServiceId = uuid_to_bin('019e0eaf-fa6e-78f1-aada-c571e0001124');
[10 May 11:33] Tomas Valinčius
It seems to work correctly if I remove the column "Position" from the final SELECT clause:

"ROW_NUMBER() OVER (PARTITION BY t.OrderServiceId ORDER BY IFNULL(t.CompServicePosition, 999999), t.Position) AS Position"
[11 May 9:03] Roy Lyseng
Thank you for the bug report.
Verified as described.