Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions mysql-test/r/group_skip_scan.result
Original file line number Diff line number Diff line change
Expand Up @@ -4316,10 +4316,8 @@ INSERT INTO t1 VALUES (NULL, 1);
INSERT INTO t1 VALUES (NULL, 2);
EXPLAIN FORMAT=TREE SELECT MIN(f2) FROM t1 WHERE f1 IS NULL GROUP BY f1;
EXPLAIN
-> Table scan on <temporary> (rows=1)
-> Temporary table with deduplication (rows=1)
-> Filter: (t1.f1 is null) (rows=1)
-> Covering index skip scan for grouping on t1 using f1 over (f1 = NULL) (rows=1)
-> Filter: (t1.f1 is null) (rows=1)
-> Covering index skip scan for grouping on t1 using f1 over (f1 = NULL) (rows=1)

SELECT MIN(f2) FROM t1 WHERE f1 IS NULL GROUP BY f1;
MIN(f2)
Expand Down
4 changes: 2 additions & 2 deletions mysql-test/r/order_by_all.result
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ a b c
1 NULL b
explain select * from t1 where a = 1 and b is null order by a desc, b desc;
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE t1 NULL ref a a 9 const,const 2 100.00 Using where; Using index; Using filesort
1 SIMPLE t1 NULL ref a a 9 const,const 2 100.00 Using where; Using index
Warnings:
Note 1003 /* select#1 */ select `test`.`t1`.`a` AS `a`,`test`.`t1`.`b` AS `b`,`test`.`t1`.`c` AS `c` from `test`.`t1` where ((`test`.`t1`.`a` = 1) and (`test`.`t1`.`b` is null)) order by `test`.`t1`.`a` desc,`test`.`t1`.`b` desc
select * from t1 where a = 1 and b is null order by a desc, b desc;
Expand All @@ -411,7 +411,7 @@ Warnings:
Note 1003 /* select#1 */ select `test`.`t1`.`a` AS `a`,`test`.`t1`.`b` AS `b`,`test`.`t1`.`c` AS `c` from `test`.`t1` where ((`test`.`t1`.`a` = 2) and (`test`.`t1`.`b` > 0)) order by `test`.`t1`.`a` desc,`test`.`t1`.`b` desc
explain select * from t1 where a = 2 and b is null order by a desc,b desc;
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE t1 NULL ref a a 9 const,const 1 100.00 Using where; Using index; Using filesort
1 SIMPLE t1 NULL ref a a 9 const,const 1 100.00 Using where; Using index
Warnings:
Note 1003 /* select#1 */ select `test`.`t1`.`a` AS `a`,`test`.`t1`.`b` AS `b`,`test`.`t1`.`c` AS `c` from `test`.`t1` where ((`test`.`t1`.`a` = 2) and (`test`.`t1`.`b` is null)) order by `test`.`t1`.`a` desc,`test`.`t1`.`b` desc
explain select * from t1 where a = 2 and (b is null or b > 0) order by a
Expand Down
4 changes: 2 additions & 2 deletions mysql-test/r/order_by_icp_mrr.result
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ a b c
1 NULL b
explain select * from t1 where a = 1 and b is null order by a desc, b desc;
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE t1 NULL ref a a 9 const,const 2 100.00 Using where; Using index; Using filesort
1 SIMPLE t1 NULL ref a a 9 const,const 2 100.00 Using where; Using index
Warnings:
Note 1003 /* select#1 */ select `test`.`t1`.`a` AS `a`,`test`.`t1`.`b` AS `b`,`test`.`t1`.`c` AS `c` from `test`.`t1` where ((`test`.`t1`.`a` = 1) and (`test`.`t1`.`b` is null)) order by `test`.`t1`.`a` desc,`test`.`t1`.`b` desc
select * from t1 where a = 1 and b is null order by a desc, b desc;
Expand All @@ -411,7 +411,7 @@ Warnings:
Note 1003 /* select#1 */ select `test`.`t1`.`a` AS `a`,`test`.`t1`.`b` AS `b`,`test`.`t1`.`c` AS `c` from `test`.`t1` where ((`test`.`t1`.`a` = 2) and (`test`.`t1`.`b` > 0)) order by `test`.`t1`.`a` desc,`test`.`t1`.`b` desc
explain select * from t1 where a = 2 and b is null order by a desc,b desc;
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE t1 NULL ref a a 9 const,const 1 100.00 Using where; Using index; Using filesort
1 SIMPLE t1 NULL ref a a 9 const,const 1 100.00 Using where; Using index
Warnings:
Note 1003 /* select#1 */ select `test`.`t1`.`a` AS `a`,`test`.`t1`.`b` AS `b`,`test`.`t1`.`c` AS `c` from `test`.`t1` where ((`test`.`t1`.`a` = 2) and (`test`.`t1`.`b` is null)) order by `test`.`t1`.`a` desc,`test`.`t1`.`b` desc
explain select * from t1 where a = 2 and (b is null or b > 0) order by a
Expand Down
259 changes: 259 additions & 0 deletions mysql-test/r/order_by_isnull.result
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
#
# Bug#119442: Optimizer doesn't simplify ORDER BY when field IS NULL
#
DROP TABLE IF EXISTS t1;
CREATE TABLE t1 (
id INT PRIMARY KEY AUTO_INCREMENT,
category_id INT NOT NULL,
nullable_date DATETIME DEFAULT NULL,
created_by VARCHAR(50),
status VARCHAR(20),
priority TINYINT,
item_name VARCHAR(100),
description VARCHAR(200),
KEY idx_covering (category_id, nullable_date, created_by, status, priority, item_name, description)
) ENGINE=InnoDB;
INSERT INTO t1 (category_id, nullable_date, created_by, status, priority, item_name, description) VALUES
(100, NULL, 'User1', 'Active', 0, 'Item A', 'Description 1'),
(100, NULL, 'User1', 'Active', 0, 'Item B', 'Description 2'),
(100, NULL, 'User1', 'Active', 0, 'Item C', 'Description 3'),
(100, NULL, 'User1', 'Pending', 0, 'Item D', 'Description 4'),
(100, '2024-01-01 10:00:00', 'User1', 'Active', 0, 'Item E', 'Description 5'),
(200, NULL, 'User2', 'Active', 1, 'Item F', 'Description 6'),
(200, NULL, 'User3', 'Pending', 0, 'Item G', 'Description 7');
ANALYZE TABLE t1;
Table Op Msg_type Msg_text
test.t1 analyze status OK
#
# Test 1: Basic IS NULL with ORDER BY - demonstrating covering index lookup
# Covering index is used for IS NULL lookup (category_id, nullable_date)
# but filesort still needed for item_name since intermediate fields aren't constant
#
EXPLAIN FORMAT=TREE
SELECT id, category_id, nullable_date, item_name
FROM t1 FORCE INDEX(idx_covering)
WHERE category_id = 100 AND nullable_date IS NULL
ORDER BY category_id, nullable_date, item_name;
EXPLAIN
-> Sort: t1.item_name (cost=0.775 rows=4)
-> Filter: (t1.nullable_date is null) (cost=0.775 rows=4)
-> Covering index lookup on t1 using idx_covering (category_id = 100, nullable_date = NULL) (cost=0.775 rows=4)

#
# Test 2: Compound index with IS NULL - demonstrating NO filesort
# Unlike Test 1, all intermediate index fields are constant, allowing
# the index to provide ordering for item_name without filesort
#
EXPLAIN FORMAT=TREE
SELECT id, category_id, status, priority, item_name, description
FROM t1 FORCE INDEX(idx_covering)
WHERE category_id = 100
AND nullable_date IS NULL
AND created_by = 'User1'
AND status = 'Active'
AND priority = 0
AND item_name >= 'Item B'
ORDER BY category_id, nullable_date, created_by, status, priority, item_name ASC
LIMIT 10;
EXPLAIN
-> Limit: 10 row(s) (cost=0.702 rows=2)
-> Filter: ((t1.priority = 0) and (t1.`status` = 'Active') and (t1.created_by = 'User1') and (t1.category_id = 100) and (t1.nullable_date is null) and (t1.item_name >= 'Item B')) (cost=0.702 rows=2)
-> Covering index range scan on t1 using idx_covering over (category_id = 100 AND nullable_date = NULL AND created_by = 'User1' AND status = 'Active' AND priority = 0 AND 'Item B' <= item_name) (cost=0.702 rows=2)

SELECT id, category_id, status, priority, item_name
FROM t1
WHERE category_id = 100
AND nullable_date IS NULL
AND created_by = 'User1'
AND status = 'Active'
AND priority = 0
AND item_name >= 'Item B'
ORDER BY category_id, nullable_date, created_by, status, priority, item_name ASC
LIMIT 10;
id category_id status priority item_name
2 100 Active 0 Item B
3 100 Active 0 Item C
#
# Test 3: Mix of equality and IS NULL with all intermediate fields constant
# All intermediate index fields are constant, so index can provide ordering
# for item_name without filesort (demonstrates IS NULL works like equality)
#
EXPLAIN FORMAT=TREE
SELECT id, category_id, nullable_date, created_by, status, priority, item_name
FROM t1 FORCE INDEX(idx_covering)
WHERE category_id = 100 AND nullable_date IS NULL AND created_by = 'User1' AND status = 'Active' AND priority = 0
ORDER BY category_id, nullable_date, created_by, status, priority, item_name;
EXPLAIN
-> Filter: (t1.nullable_date is null) (cost=0.35 rows=1)
-> Covering index lookup on t1 using idx_covering (category_id = 100, nullable_date = NULL, created_by = 'User1', status = 'Active', priority = 0) (cost=0.35 rows=1)

#
# Test 4: IS NOT NULL should NOT be treated as constant
# This is a negative test - field can have many different non-NULL values
#
EXPLAIN FORMAT=TREE
SELECT id, category_id, nullable_date, item_name
FROM t1
WHERE nullable_date IS NOT NULL
ORDER BY nullable_date, item_name;
EXPLAIN
-> Sort: t1.nullable_date, t1.item_name (cost=0.95 rows=7)
-> Filter: (t1.nullable_date is not null) (cost=0.95 rows=7)
-> Covering index scan on t1 using idx_covering (cost=0.95 rows=7)

#
# Test 5: Test with optimizer trace to verify "equals_constant_in_where"
# The trace should show that nullable_date is recognized as constant
#
SET optimizer_trace="enabled=on";
SELECT id, category_id, nullable_date, item_name
FROM t1 FORCE INDEX(idx_covering)
WHERE category_id = 100 AND nullable_date IS NULL
ORDER BY category_id, nullable_date, item_name
LIMIT 2;
id category_id nullable_date item_name
1 100 NULL Item A
2 100 NULL Item B
SELECT IF(
LOCATE('"equals_constant_in_where": true', TRACE) > 0,
'PASS: nullable_date recognized as constant',
'FAIL: nullable_date not recognized as constant'
) AS trace_check
FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;
trace_check
PASS: nullable_date recognized as constant
SET optimizer_trace="enabled=off";
#
# Test 6: OR with IS NULL - field should NOT be treated as constant
# (nullable_date can be NULL OR have a value, so it's not constant)
#
EXPLAIN FORMAT=TREE
SELECT id, category_id, nullable_date, item_name
FROM t1
WHERE category_id = 100 AND (nullable_date IS NULL OR nullable_date > '2024-01-01')
ORDER BY nullable_date, item_name;
EXPLAIN
-> Sort: t1.nullable_date, t1.item_name (cost=1.43 rows=5)
-> Filter: ((t1.category_id = 100) and ((t1.nullable_date is null) or (t1.nullable_date > TIMESTAMP'2024-01-01 00:00:00'))) (cost=1.43 rows=5)
-> Covering index range scan on t1 using idx_covering over (category_id = 100 AND nullable_date = NULL) OR (category_id = 100 AND '2024-01-01 00:00:00' < nullable_date) (cost=1.43 rows=5)

SELECT id, category_id, nullable_date, item_name
FROM t1
WHERE category_id = 100 AND (nullable_date IS NULL OR nullable_date > '2024-01-01')
ORDER BY nullable_date, item_name;
id category_id nullable_date item_name
1 100 NULL Item A
2 100 NULL Item B
3 100 NULL Item C
4 100 NULL Item D
5 100 2024-01-01 10:00:00 Item E
#
# Test 7: OR with multiple IS NULL - same field
# This is effectively the same as a single IS NULL, so field IS constant
#
EXPLAIN FORMAT=TREE
SELECT id, category_id, nullable_date, item_name
FROM t1 FORCE INDEX(idx_covering)
WHERE category_id = 100 AND (nullable_date IS NULL OR nullable_date IS NULL)
ORDER BY category_id, nullable_date, item_name;
EXPLAIN
-> Sort: t1.item_name (cost=0.775 rows=4)
-> Filter: ((t1.nullable_date is null) or (t1.nullable_date is null)) (cost=0.775 rows=4)
-> Covering index lookup on t1 using idx_covering (category_id = 100, nullable_date = NULL) (cost=0.775 rows=4)

#
# Test 8: Verify IS NULL works with DESC ordering
# When nullable_date IS NULL with other constants, index can still be used
# even with DESC on the non-constant field
#
EXPLAIN FORMAT=TREE
SELECT id, category_id, nullable_date, created_by, status, priority, item_name
FROM t1 FORCE INDEX(idx_covering)
WHERE category_id = 100 AND nullable_date IS NULL AND created_by = 'User1' AND status = 'Active' AND priority = 0
ORDER BY category_id, nullable_date, created_by, status, priority, item_name DESC;
EXPLAIN
-> Filter: (t1.nullable_date is null) (cost=0.633 rows=3)
-> Covering index lookup on t1 using idx_covering (category_id = 100, nullable_date = NULL, created_by = 'User1', status = 'Active', priority = 0) (reverse) (cost=0.633 rows=3)

#
# Test 9: Verify NULL ordering semantics with ASC
# NULLs should appear FIRST with ORDER BY ASC
#
CREATE TABLE t_null_order (
id INT PRIMARY KEY AUTO_INCREMENT,
val INT,
name VARCHAR(10),
KEY idx_val (val, name)
);
INSERT INTO t_null_order (val, name) VALUES
(NULL, 'null1'),
(NULL, 'null2'),
(5, 'five'),
(10, 'ten'),
(NULL, 'null3'),
(1, 'one');
ANALYZE TABLE t_null_order;
Table Op Msg_type Msg_text
test.t_null_order analyze status OK
SELECT id, val, name FROM t_null_order ORDER BY val ASC, name ASC;
id val name
1 NULL null1
2 NULL null2
5 NULL null3
6 1 one
3 5 five
4 10 ten
#
# Test 10: Verify NULL ordering semantics with DESC
# NULLs should appear LAST with ORDER BY DESC
#
SELECT id, val, name FROM t_null_order ORDER BY val DESC, name ASC;
id val name
4 10 ten
3 5 five
6 1 one
1 NULL null1
2 NULL null2
5 NULL null3
#
# Test 11: Verify optimization works with DESC and preserves NULL ordering
# With val IS NULL, val is constant, so should only sort by name
#
EXPLAIN FORMAT=TREE
SELECT id, val, name FROM t_null_order FORCE INDEX(idx_val)
WHERE val IS NULL
ORDER BY val DESC, name ASC;
EXPLAIN
-> Filter: (t_null_order.val is null) (cost=0.553 rows=3)
-> Covering index lookup on t_null_order using idx_val (val = NULL) (cost=0.553 rows=3)

SELECT id, val, name FROM t_null_order FORCE INDEX(idx_val)
WHERE val IS NULL
ORDER BY val DESC, name ASC;
id val name
1 NULL null1
2 NULL null2
5 NULL null3
#
# Test 12: Mixed ASC/DESC with IS NULL constraint
#
EXPLAIN FORMAT=TREE
SELECT id, val, name FROM t_null_order FORCE INDEX(idx_val)
WHERE val IS NULL
ORDER BY val ASC, name DESC;
EXPLAIN
-> Filter: (t_null_order.val is null) (cost=0.553 rows=3)
-> Covering index lookup on t_null_order using idx_val (val = NULL) (reverse) (cost=0.553 rows=3)

SELECT id, val, name FROM t_null_order FORCE INDEX(idx_val)
WHERE val IS NULL
ORDER BY val ASC, name DESC;
id val name
5 NULL null3
2 NULL null2
1 NULL null1
DROP TABLE t_null_order;
DROP TABLE t1;
#
# End of test for Bug#119442
#
4 changes: 2 additions & 2 deletions mysql-test/r/order_by_none.result
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ a b c
1 NULL b
explain select * from t1 where a = 1 and b is null order by a desc, b desc;
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE t1 NULL ref a a 9 const,const 2 100.00 Using where; Using index; Using filesort
1 SIMPLE t1 NULL ref a a 9 const,const 2 100.00 Using where; Using index
Warnings:
Note 1003 /* select#1 */ select `test`.`t1`.`a` AS `a`,`test`.`t1`.`b` AS `b`,`test`.`t1`.`c` AS `c` from `test`.`t1` where ((`test`.`t1`.`a` = 1) and (`test`.`t1`.`b` is null)) order by `test`.`t1`.`a` desc,`test`.`t1`.`b` desc
select * from t1 where a = 1 and b is null order by a desc, b desc;
Expand All @@ -410,7 +410,7 @@ Warnings:
Note 1003 /* select#1 */ select `test`.`t1`.`a` AS `a`,`test`.`t1`.`b` AS `b`,`test`.`t1`.`c` AS `c` from `test`.`t1` where ((`test`.`t1`.`a` = 2) and (`test`.`t1`.`b` > 0)) order by `test`.`t1`.`a` desc,`test`.`t1`.`b` desc
explain select * from t1 where a = 2 and b is null order by a desc,b desc;
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE t1 NULL ref a a 9 const,const 1 100.00 Using where; Using index; Using filesort
1 SIMPLE t1 NULL ref a a 9 const,const 1 100.00 Using where; Using index
Warnings:
Note 1003 /* select#1 */ select `test`.`t1`.`a` AS `a`,`test`.`t1`.`b` AS `b`,`test`.`t1`.`c` AS `c` from `test`.`t1` where ((`test`.`t1`.`a` = 2) and (`test`.`t1`.`b` is null)) order by `test`.`t1`.`a` desc,`test`.`t1`.`b` desc
explain select * from t1 where a = 2 and (b is null or b > 0) order by a
Expand Down
Loading