Description:
Instead of index on incidents.c_id (or index merge on all the indexed columns) optimizer uses the index on column chacked for IS NULL:
mysql> explain select count(*) from contacts contacts LEFT JOIN incidents incidents ON
-> contacts.c_id = incidents.c_id WHERE ((incidents.disp_lvl4_id IN (275)
-> OR incidents.disp_lvl1_id IN (362)) AND ( ((incidents.cat_lvl2_id IS
-> NULL)))) AND incidents.c_id=1061546\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: contacts
type: const
possible_keys: contacts$c_id
key: contacts$c_id
key_len: 4
ref: const
rows: 1
Extra: Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: incidents
type: ref
possible_keys: incidents$c_id,incidents$cat_lvl2,incidents$disp_lvl1
key: incidents$cat_lvl2
key_len: 3
ref: const
rows: 10
Extra: Using where
2 rows in set (0.05 sec)
Note estimated number of rows, totally wrong. In reality this plan leads to scanning million(s) of rows:
mysql> show status like 'Handler_read%';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Handler_read_first | 0 |
| Handler_read_key | 3 |
| Handler_read_next | 0 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 26 |
+-----------------------+-------+
6 rows in set (0.00 sec)
mysql> select count(*) from contacts contacts LEFT JOIN incidents incidents ON contacts.c_id = incidents.c_id WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362)) AND ( ((incidents.cat_lvl2_id IS NULL)))) AND incidents.c_id=1061546\G
*************************** 1. row ***************************
count(*): 0
1 row in set (34.92 sec)
mysql> show status like 'Handler_read%';
+-----------------------+---------+
| Variable_name | Value |
+-----------------------+---------+
| Handler_read_first | 0 |
| Handler_read_key | 8 |
| Handler_read_next | 1889342 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 33 |
+-----------------------+---------+
6 rows in set (0.00 sec)
While when index on c_id is used query runs instantly:
mysql> select count(*) from contacts contacts LEFT JOIN incidents incidents FORCE INDEX(`incidents$c_id`) ON contacts.c_id = incidents.c_id WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362)) AND ( ((incidents.cat_lvl2_id IS NULL)))) AND incidents.c_id=1061546\G
*************************** 1. row ***************************
count(*): 0
1 row in set (0.09 sec)
Note also the following:
mysql> show create table incidents\G
*************************** 1. row ***************************
Table: incidents
Create Table: CREATE TABLE `incidents` (
`i_id` int(11) NOT NULL default '0',
`cat_lvl2_id` smallint(6) default NULL,
`c_id` int(11) default NULL,
`disp_lvl1_id` smallint(6) default NULL,
`disp_lvl4_id` smallint(6) default NULL,
UNIQUE KEY `incidents$i_id` (`i_id`),
KEY `incidents$c_id` (`c_id`),
KEY `incidents$cat_lvl2` (`cat_lvl2_id`),
KEY `incidents$disp_lvl1` (`disp_lvl1_id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1
1 row in set (0.04 sec)
mysql> select count(*) from incidents where c_id=1061546;
+----------+
| count(*) |
+----------+
| 52 |
+----------+
1 row in set (0.00 sec)
mysql> select count(*) from incidents where cat_lvl2_id is null;
+----------+
| count(*) |
+----------+
| 1889342 |
+----------+
1 row in set (1.95 sec)
mysql> select count(*) from incidents where disp_lvl1_id IN (362);
+----------+
| count(*) |
+----------+
| 2915 |
+----------+
1 row in set (0.01 sec)
Same plan is generated for both InnoDB and MyISAM tables, ANALYZE does not help.
For inner JOIN optimizer does a good job:
mysql> explain select count(*) from contacts contacts JOIN incidents incidents ON contacts.c_id = incidents.c_id WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362)) AND ( ((incidents.cat_lvl2_id IS NULL)))) AND incidents.c_id=1061546\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: contacts
type: const
possible_keys: contacts$c_id
key: contacts$c_id
key_len: 4
ref: const
rows: 1
Extra: Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: incidents
type: ref
possible_keys: incidents$c_id,incidents$cat_lvl2,incidents$disp_lvl1
key: incidents$c_id
key_len: 5
ref: const
rows: 70
Extra: Using where
2 rows in set (0.02 sec)
mysql> select count(*) from contacts contacts JOIN incidents incidents ON contacts.c_id = incidents.c_id WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362)) AND ( ((incidents.cat_lvl2_id IS NULL)))) AND incidents.c_id=1061546\G
*************************** 1. row ***************************
count(*): 0
1 row in set (0.00 sec)
but OUTER should NOT matter in this case.
How to repeat:
Load data (uploaded to the FTP server). Then:
analyze table incidents;
analyze table contacts;
explain select count(*) from contacts contacts
LEFT JOIN incidents FORCE INDEX (`incidents$disp_lvl1`)
ON contacts.c_id = incidents.c_id
WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362))
AND ( ((incidents.cat_lvl2_id IS NULL))))\G
explain select count(*) from contacts contacts
LEFT JOIN incidents
ON contacts.c_id = incidents.c_id
WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362))
AND ( ((incidents.cat_lvl2_id IS NULL))))\G
explain select count(*) from contacts contacts
JOIN incidents
ON contacts.c_id = incidents.c_id
WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362))
AND ( ((incidents.cat_lvl2_id IS NULL))))\G
select count(*) from contacts contacts
LEFT JOIN incidents FORCE INDEX (`incidents$disp_lvl1`)
ON contacts.c_id = incidents.c_id
WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362))
AND ( ((incidents.cat_lvl2_id IS NULL))));
select count(*) from contacts contacts
LEFT JOIN incidents
ON contacts.c_id = incidents.c_id
WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362))
AND ( ((incidents.cat_lvl2_id IS NULL))));
select count(*) from contacts contacts
JOIN incidents
ON contacts.c_id = incidents.c_id
WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362))
AND ( ((incidents.cat_lvl2_id IS NULL))))\G
Compare plans and execution times (or number of rows accessed, based on 'Handler_read_%' values before and after each SELECT.
Suggested fix:
Use proper index(es) for right table in LEFT JOIN?
Description: Instead of index on incidents.c_id (or index merge on all the indexed columns) optimizer uses the index on column chacked for IS NULL: mysql> explain select count(*) from contacts contacts LEFT JOIN incidents incidents ON -> contacts.c_id = incidents.c_id WHERE ((incidents.disp_lvl4_id IN (275) -> OR incidents.disp_lvl1_id IN (362)) AND ( ((incidents.cat_lvl2_id IS -> NULL)))) AND incidents.c_id=1061546\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: contacts type: const possible_keys: contacts$c_id key: contacts$c_id key_len: 4 ref: const rows: 1 Extra: Using index *************************** 2. row *************************** id: 1 select_type: SIMPLE table: incidents type: ref possible_keys: incidents$c_id,incidents$cat_lvl2,incidents$disp_lvl1 key: incidents$cat_lvl2 key_len: 3 ref: const rows: 10 Extra: Using where 2 rows in set (0.05 sec) Note estimated number of rows, totally wrong. In reality this plan leads to scanning million(s) of rows: mysql> show status like 'Handler_read%'; +-----------------------+-------+ | Variable_name | Value | +-----------------------+-------+ | Handler_read_first | 0 | | Handler_read_key | 3 | | Handler_read_next | 0 | | Handler_read_prev | 0 | | Handler_read_rnd | 0 | | Handler_read_rnd_next | 26 | +-----------------------+-------+ 6 rows in set (0.00 sec) mysql> select count(*) from contacts contacts LEFT JOIN incidents incidents ON contacts.c_id = incidents.c_id WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362)) AND ( ((incidents.cat_lvl2_id IS NULL)))) AND incidents.c_id=1061546\G *************************** 1. row *************************** count(*): 0 1 row in set (34.92 sec) mysql> show status like 'Handler_read%'; +-----------------------+---------+ | Variable_name | Value | +-----------------------+---------+ | Handler_read_first | 0 | | Handler_read_key | 8 | | Handler_read_next | 1889342 | | Handler_read_prev | 0 | | Handler_read_rnd | 0 | | Handler_read_rnd_next | 33 | +-----------------------+---------+ 6 rows in set (0.00 sec) While when index on c_id is used query runs instantly: mysql> select count(*) from contacts contacts LEFT JOIN incidents incidents FORCE INDEX(`incidents$c_id`) ON contacts.c_id = incidents.c_id WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362)) AND ( ((incidents.cat_lvl2_id IS NULL)))) AND incidents.c_id=1061546\G *************************** 1. row *************************** count(*): 0 1 row in set (0.09 sec) Note also the following: mysql> show create table incidents\G *************************** 1. row *************************** Table: incidents Create Table: CREATE TABLE `incidents` ( `i_id` int(11) NOT NULL default '0', `cat_lvl2_id` smallint(6) default NULL, `c_id` int(11) default NULL, `disp_lvl1_id` smallint(6) default NULL, `disp_lvl4_id` smallint(6) default NULL, UNIQUE KEY `incidents$i_id` (`i_id`), KEY `incidents$c_id` (`c_id`), KEY `incidents$cat_lvl2` (`cat_lvl2_id`), KEY `incidents$disp_lvl1` (`disp_lvl1_id`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1 1 row in set (0.04 sec) mysql> select count(*) from incidents where c_id=1061546; +----------+ | count(*) | +----------+ | 52 | +----------+ 1 row in set (0.00 sec) mysql> select count(*) from incidents where cat_lvl2_id is null; +----------+ | count(*) | +----------+ | 1889342 | +----------+ 1 row in set (1.95 sec) mysql> select count(*) from incidents where disp_lvl1_id IN (362); +----------+ | count(*) | +----------+ | 2915 | +----------+ 1 row in set (0.01 sec) Same plan is generated for both InnoDB and MyISAM tables, ANALYZE does not help. For inner JOIN optimizer does a good job: mysql> explain select count(*) from contacts contacts JOIN incidents incidents ON contacts.c_id = incidents.c_id WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362)) AND ( ((incidents.cat_lvl2_id IS NULL)))) AND incidents.c_id=1061546\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: contacts type: const possible_keys: contacts$c_id key: contacts$c_id key_len: 4 ref: const rows: 1 Extra: Using index *************************** 2. row *************************** id: 1 select_type: SIMPLE table: incidents type: ref possible_keys: incidents$c_id,incidents$cat_lvl2,incidents$disp_lvl1 key: incidents$c_id key_len: 5 ref: const rows: 70 Extra: Using where 2 rows in set (0.02 sec) mysql> select count(*) from contacts contacts JOIN incidents incidents ON contacts.c_id = incidents.c_id WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362)) AND ( ((incidents.cat_lvl2_id IS NULL)))) AND incidents.c_id=1061546\G *************************** 1. row *************************** count(*): 0 1 row in set (0.00 sec) but OUTER should NOT matter in this case. How to repeat: Load data (uploaded to the FTP server). Then: analyze table incidents; analyze table contacts; explain select count(*) from contacts contacts LEFT JOIN incidents FORCE INDEX (`incidents$disp_lvl1`) ON contacts.c_id = incidents.c_id WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362)) AND ( ((incidents.cat_lvl2_id IS NULL))))\G explain select count(*) from contacts contacts LEFT JOIN incidents ON contacts.c_id = incidents.c_id WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362)) AND ( ((incidents.cat_lvl2_id IS NULL))))\G explain select count(*) from contacts contacts JOIN incidents ON contacts.c_id = incidents.c_id WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362)) AND ( ((incidents.cat_lvl2_id IS NULL))))\G select count(*) from contacts contacts LEFT JOIN incidents FORCE INDEX (`incidents$disp_lvl1`) ON contacts.c_id = incidents.c_id WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362)) AND ( ((incidents.cat_lvl2_id IS NULL)))); select count(*) from contacts contacts LEFT JOIN incidents ON contacts.c_id = incidents.c_id WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362)) AND ( ((incidents.cat_lvl2_id IS NULL)))); select count(*) from contacts contacts JOIN incidents ON contacts.c_id = incidents.c_id WHERE ((incidents.disp_lvl4_id IN (275) OR incidents.disp_lvl1_id IN (362)) AND ( ((incidents.cat_lvl2_id IS NULL))))\G Compare plans and execution times (or number of rows accessed, based on 'Handler_read_%' values before and after each SELECT. Suggested fix: Use proper index(es) for right table in LEFT JOIN?