Skip to content

Commit 87df393

Browse files
committed
Bug#119442 ORDER BY optimization doesn't recognize IS NULL as constant
PROBLEM: 1. The optimizer's check_field_is_const() function only recognized equality operators (col = value) as constants for ORDER BY simplification. 2. It failed to recognize that "col IS NULL" also guarantees a single value (NULL) for sorting purposes. 3. This caused unnecessary filesort operations even when covering indexes existed. 4. Example: WHERE a = 1 AND b IS NULL ORDER BY a, b, c would use filesort despite having index (a, b, c). FIX: 1. Extended check_field_is_const() to handle Item_func::ISNULL_FUNC. 2. Added is_isnull_func() helper function to identify IS NULL conditions. 3. Properly handles OR conditions - "IS NULL OR value" remains non-constant, while "IS NULL OR IS NULL" is constant. 4. Stores IS NULL function as sentinel in const_item to ensure consistency across OR branches. 5. Preserves correct NULL ordering semantics (NULLs first in ASC, last in DESC).
1 parent b79ac11 commit 87df393

File tree

5 files changed

+530
-18
lines changed

5 files changed

+530
-18
lines changed

mysql-test/r/order_by_all.result

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ a b c
392392
1 NULL b
393393
explain select * from t1 where a = 1 and b is null order by a desc, b desc;
394394
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
395-
1 SIMPLE t1 NULL ref a a 9 const,const 2 100.00 Using where; Using index; Using filesort
395+
1 SIMPLE t1 NULL ref a a 9 const,const 2 100.00 Using where; Using index
396396
Warnings:
397397
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
398398
select * from t1 where a = 1 and b is null order by a desc, b desc;
@@ -411,7 +411,7 @@ Warnings:
411411
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
412412
explain select * from t1 where a = 2 and b is null order by a desc,b desc;
413413
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
414-
1 SIMPLE t1 NULL ref a a 9 const,const 1 100.00 Using where; Using index; Using filesort
414+
1 SIMPLE t1 NULL ref a a 9 const,const 1 100.00 Using where; Using index
415415
Warnings:
416416
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
417417
explain select * from t1 where a = 2 and (b is null or b > 0) order by a
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
#
2+
# Bug#119442: Optimizer doesn't simplify ORDER BY when field IS NULL
3+
#
4+
DROP TABLE IF EXISTS t1;
5+
CREATE TABLE t1 (
6+
id INT PRIMARY KEY AUTO_INCREMENT,
7+
category_id INT NOT NULL,
8+
nullable_date DATETIME DEFAULT NULL,
9+
created_by VARCHAR(50),
10+
status VARCHAR(20),
11+
priority TINYINT,
12+
item_name VARCHAR(100),
13+
description VARCHAR(200),
14+
KEY idx_covering (category_id, nullable_date, created_by, status, priority, item_name, description)
15+
) ENGINE=InnoDB;
16+
INSERT INTO t1 (category_id, nullable_date, created_by, status, priority, item_name, description) VALUES
17+
(100, NULL, 'User1', 'Active', 0, 'Item A', 'Description 1'),
18+
(100, NULL, 'User1', 'Active', 0, 'Item B', 'Description 2'),
19+
(100, NULL, 'User1', 'Active', 0, 'Item C', 'Description 3'),
20+
(100, NULL, 'User1', 'Pending', 0, 'Item D', 'Description 4'),
21+
(100, '2024-01-01 10:00:00', 'User1', 'Active', 0, 'Item E', 'Description 5'),
22+
(200, NULL, 'User2', 'Active', 1, 'Item F', 'Description 6'),
23+
(200, NULL, 'User3', 'Pending', 0, 'Item G', 'Description 7');
24+
ANALYZE TABLE t1;
25+
Table Op Msg_type Msg_text
26+
test.t1 analyze status OK
27+
#
28+
# Test 1: Basic IS NULL with ORDER BY - demonstrating covering index lookup
29+
# Covering index is used for IS NULL lookup (category_id, nullable_date)
30+
# but filesort still needed for item_name since intermediate fields aren't constant
31+
#
32+
EXPLAIN FORMAT=TREE
33+
SELECT id, category_id, nullable_date, item_name
34+
FROM t1 FORCE INDEX(idx_covering)
35+
WHERE category_id = 100 AND nullable_date IS NULL
36+
ORDER BY category_id, nullable_date, item_name;
37+
EXPLAIN
38+
-> Sort: t1.item_name (cost=0.775 rows=4)
39+
-> Filter: (t1.nullable_date is null) (cost=0.775 rows=4)
40+
-> Covering index lookup on t1 using idx_covering (category_id = 100, nullable_date = NULL) (cost=0.775 rows=4)
41+
42+
#
43+
# Test 2: Compound index with IS NULL - demonstrating NO filesort
44+
# Unlike Test 1, all intermediate index fields are constant, allowing
45+
# the index to provide ordering for item_name without filesort
46+
#
47+
EXPLAIN FORMAT=TREE
48+
SELECT id, category_id, status, priority, item_name, description
49+
FROM t1 FORCE INDEX(idx_covering)
50+
WHERE category_id = 100
51+
AND nullable_date IS NULL
52+
AND created_by = 'User1'
53+
AND status = 'Active'
54+
AND priority = 0
55+
AND item_name >= 'Item B'
56+
ORDER BY category_id, nullable_date, created_by, status, priority, item_name ASC
57+
LIMIT 10;
58+
EXPLAIN
59+
-> Limit: 10 row(s) (cost=0.702 rows=2)
60+
-> 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)
61+
-> 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)
62+
63+
SELECT id, category_id, status, priority, item_name
64+
FROM t1
65+
WHERE category_id = 100
66+
AND nullable_date IS NULL
67+
AND created_by = 'User1'
68+
AND status = 'Active'
69+
AND priority = 0
70+
AND item_name >= 'Item B'
71+
ORDER BY category_id, nullable_date, created_by, status, priority, item_name ASC
72+
LIMIT 10;
73+
id category_id status priority item_name
74+
2 100 Active 0 Item B
75+
3 100 Active 0 Item C
76+
#
77+
# Test 3: Mix of equality and IS NULL with all intermediate fields constant
78+
# All intermediate index fields are constant, so index can provide ordering
79+
# for item_name without filesort (demonstrates IS NULL works like equality)
80+
#
81+
EXPLAIN FORMAT=TREE
82+
SELECT id, category_id, nullable_date, created_by, status, priority, item_name
83+
FROM t1 FORCE INDEX(idx_covering)
84+
WHERE category_id = 100 AND nullable_date IS NULL AND created_by = 'User1' AND status = 'Active' AND priority = 0
85+
ORDER BY category_id, nullable_date, created_by, status, priority, item_name;
86+
EXPLAIN
87+
-> Filter: (t1.nullable_date is null) (cost=0.35 rows=1)
88+
-> 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)
89+
90+
#
91+
# Test 4: IS NOT NULL should NOT be treated as constant
92+
# This is a negative test - field can have many different non-NULL values
93+
#
94+
EXPLAIN FORMAT=TREE
95+
SELECT id, category_id, nullable_date, item_name
96+
FROM t1
97+
WHERE nullable_date IS NOT NULL
98+
ORDER BY nullable_date, item_name;
99+
EXPLAIN
100+
-> Sort: t1.nullable_date, t1.item_name (cost=0.95 rows=7)
101+
-> Filter: (t1.nullable_date is not null) (cost=0.95 rows=7)
102+
-> Covering index scan on t1 using idx_covering (cost=0.95 rows=7)
103+
104+
#
105+
# Test 5: Test with optimizer trace to verify "equals_constant_in_where"
106+
# The trace should show that nullable_date is recognized as constant
107+
#
108+
SET optimizer_trace="enabled=on";
109+
SELECT id, category_id, nullable_date, item_name
110+
FROM t1 FORCE INDEX(idx_covering)
111+
WHERE category_id = 100 AND nullable_date IS NULL
112+
ORDER BY category_id, nullable_date, item_name
113+
LIMIT 2;
114+
id category_id nullable_date item_name
115+
1 100 NULL Item A
116+
2 100 NULL Item B
117+
SELECT IF(
118+
LOCATE('"equals_constant_in_where": true', TRACE) > 0,
119+
'PASS: nullable_date recognized as constant',
120+
'FAIL: nullable_date not recognized as constant'
121+
) AS trace_check
122+
FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;
123+
trace_check
124+
PASS: nullable_date recognized as constant
125+
SET optimizer_trace="enabled=off";
126+
#
127+
# Test 6: OR with IS NULL - field should NOT be treated as constant
128+
# (nullable_date can be NULL OR have a value, so it's not constant)
129+
#
130+
EXPLAIN FORMAT=TREE
131+
SELECT id, category_id, nullable_date, item_name
132+
FROM t1
133+
WHERE category_id = 100 AND (nullable_date IS NULL OR nullable_date > '2024-01-01')
134+
ORDER BY nullable_date, item_name;
135+
EXPLAIN
136+
-> Sort: t1.nullable_date, t1.item_name (cost=1.43 rows=5)
137+
-> 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)
138+
-> 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)
139+
140+
SELECT id, category_id, nullable_date, item_name
141+
FROM t1
142+
WHERE category_id = 100 AND (nullable_date IS NULL OR nullable_date > '2024-01-01')
143+
ORDER BY nullable_date, item_name;
144+
id category_id nullable_date item_name
145+
1 100 NULL Item A
146+
2 100 NULL Item B
147+
3 100 NULL Item C
148+
4 100 NULL Item D
149+
5 100 2024-01-01 10:00:00 Item E
150+
#
151+
# Test 7: OR with multiple IS NULL - same field
152+
# This is effectively the same as a single IS NULL, so field IS constant
153+
#
154+
EXPLAIN FORMAT=TREE
155+
SELECT id, category_id, nullable_date, item_name
156+
FROM t1 FORCE INDEX(idx_covering)
157+
WHERE category_id = 100 AND (nullable_date IS NULL OR nullable_date IS NULL)
158+
ORDER BY category_id, nullable_date, item_name;
159+
EXPLAIN
160+
-> Sort: t1.item_name (cost=0.775 rows=4)
161+
-> Filter: ((t1.nullable_date is null) or (t1.nullable_date is null)) (cost=0.775 rows=4)
162+
-> Covering index lookup on t1 using idx_covering (category_id = 100, nullable_date = NULL) (cost=0.775 rows=4)
163+
164+
#
165+
# Test 8: Verify IS NULL works with DESC ordering
166+
# When nullable_date IS NULL with other constants, index can still be used
167+
# even with DESC on the non-constant field
168+
#
169+
EXPLAIN FORMAT=TREE
170+
SELECT id, category_id, nullable_date, created_by, status, priority, item_name
171+
FROM t1 FORCE INDEX(idx_covering)
172+
WHERE category_id = 100 AND nullable_date IS NULL AND created_by = 'User1' AND status = 'Active' AND priority = 0
173+
ORDER BY category_id, nullable_date, created_by, status, priority, item_name DESC;
174+
EXPLAIN
175+
-> Filter: (t1.nullable_date is null) (cost=0.633 rows=3)
176+
-> 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)
177+
178+
#
179+
# Test 9: Verify NULL ordering semantics with ASC
180+
# NULLs should appear FIRST with ORDER BY ASC
181+
#
182+
CREATE TABLE t_null_order (
183+
id INT PRIMARY KEY AUTO_INCREMENT,
184+
val INT,
185+
name VARCHAR(10),
186+
KEY idx_val (val, name)
187+
);
188+
INSERT INTO t_null_order (val, name) VALUES
189+
(NULL, 'null1'),
190+
(NULL, 'null2'),
191+
(5, 'five'),
192+
(10, 'ten'),
193+
(NULL, 'null3'),
194+
(1, 'one');
195+
ANALYZE TABLE t_null_order;
196+
Table Op Msg_type Msg_text
197+
test.t_null_order analyze status OK
198+
SELECT id, val, name FROM t_null_order ORDER BY val ASC, name ASC;
199+
id val name
200+
1 NULL null1
201+
2 NULL null2
202+
5 NULL null3
203+
6 1 one
204+
3 5 five
205+
4 10 ten
206+
#
207+
# Test 10: Verify NULL ordering semantics with DESC
208+
# NULLs should appear LAST with ORDER BY DESC
209+
#
210+
SELECT id, val, name FROM t_null_order ORDER BY val DESC, name ASC;
211+
id val name
212+
4 10 ten
213+
3 5 five
214+
6 1 one
215+
1 NULL null1
216+
2 NULL null2
217+
5 NULL null3
218+
#
219+
# Test 11: Verify optimization works with DESC and preserves NULL ordering
220+
# With val IS NULL, val is constant, so should only sort by name
221+
#
222+
EXPLAIN FORMAT=TREE
223+
SELECT id, val, name FROM t_null_order FORCE INDEX(idx_val)
224+
WHERE val IS NULL
225+
ORDER BY val DESC, name ASC;
226+
EXPLAIN
227+
-> Filter: (t_null_order.val is null) (cost=0.553 rows=3)
228+
-> Covering index lookup on t_null_order using idx_val (val = NULL) (cost=0.553 rows=3)
229+
230+
SELECT id, val, name FROM t_null_order FORCE INDEX(idx_val)
231+
WHERE val IS NULL
232+
ORDER BY val DESC, name ASC;
233+
id val name
234+
1 NULL null1
235+
2 NULL null2
236+
5 NULL null3
237+
#
238+
# Test 12: Mixed ASC/DESC with IS NULL constraint
239+
#
240+
EXPLAIN FORMAT=TREE
241+
SELECT id, val, name FROM t_null_order FORCE INDEX(idx_val)
242+
WHERE val IS NULL
243+
ORDER BY val ASC, name DESC;
244+
EXPLAIN
245+
-> Filter: (t_null_order.val is null) (cost=0.553 rows=3)
246+
-> Covering index lookup on t_null_order using idx_val (val = NULL) (reverse) (cost=0.553 rows=3)
247+
248+
SELECT id, val, name FROM t_null_order FORCE INDEX(idx_val)
249+
WHERE val IS NULL
250+
ORDER BY val ASC, name DESC;
251+
id val name
252+
5 NULL null3
253+
2 NULL null2
254+
1 NULL null1
255+
DROP TABLE t_null_order;
256+
DROP TABLE t1;
257+
#
258+
# End of test for Bug#119442
259+
#

mysql-test/r/order_by_none.result

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ a b c
391391
1 NULL b
392392
explain select * from t1 where a = 1 and b is null order by a desc, b desc;
393393
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
394-
1 SIMPLE t1 NULL ref a a 9 const,const 2 100.00 Using where; Using index; Using filesort
394+
1 SIMPLE t1 NULL ref a a 9 const,const 2 100.00 Using where; Using index
395395
Warnings:
396396
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
397397
select * from t1 where a = 1 and b is null order by a desc, b desc;
@@ -410,7 +410,7 @@ Warnings:
410410
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
411411
explain select * from t1 where a = 2 and b is null order by a desc,b desc;
412412
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
413-
1 SIMPLE t1 NULL ref a a 9 const,const 1 100.00 Using where; Using index; Using filesort
413+
1 SIMPLE t1 NULL ref a a 9 const,const 1 100.00 Using where; Using index
414414
Warnings:
415415
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
416416
explain select * from t1 where a = 2 and (b is null or b > 0) order by a

0 commit comments

Comments
 (0)