为什么从 SQL 查询中删除 BINARY 函数调用会如此显着地改变查询计划?

ran*_*101 4 mysql sql collation query-optimization query-planner

我有一个 SQL 查询,它在表中查找特定值,然后在三个表之间进行内部联接以获取结果集。这三个表是fabric_barcode_oc, fabric_barcode_items&fabric_barcode_rolls

初始查询

查询的初始版本如下

EXPLAIN ANALYZE
SELECT `oc`.`oc_number` AS `ocNumber` , `roll`.`po_number` AS `poNumber` ,
`item`.`item_code` AS `itemCode` , `roll`.`roll_length` AS `rollLength` ,
`roll`.`roll_utilized` AS `rollUtilized`
FROM `fabric_barcode_rolls` AS `roll`
INNER JOIN `fabric_barcode_oc` AS `oc` ON `oc`.`oc_unique_id` = `roll`.`oc_unique_id`
INNER JOIN `fabric_barcode_items` AS `item` ON `item`.`item_unique_id` = `roll`.`item_unique_id_fk`
WHERE BINARY `roll`.`roll_number` = 'dZkzHJ_je8'
Run Code Online (Sandbox Code Playgroud)

EXPLAIN ANALYZE在此运行时,我得到以下信息

"-> Nested loop inner join  (cost=468160.85 rows=582047) (actual time=0.063..254.186 rows=1 loops=1)
    -> Nested loop inner join  (cost=264444.40 rows=582047) (actual time=0.057..254.179 rows=1 loops=1)
        -> Filter: (cast(roll.roll_number as char charset binary) = 'dZkzHJ_je8')  (cost=60727.95 rows=582047) (actual time=0.047..254.169 rows=1 loops=1)
            -> Table scan on roll  (cost=60727.95 rows=582047) (actual time=0.042..198.634 rows=599578 loops=1)
        -> Single-row index lookup on oc using PRIMARY (oc_unique_id=roll.oc_unique_id)  (cost=0.25 rows=1) (actual time=0.009..0.009 rows=1 loops=1)
    -> Single-row index lookup on item using PRIMARY (item_unique_id=roll.item_unique_id_fk)  (cost=0.25 rows=1) (actual time=0.006..0.006 rows=1 loops=1)
"
Run Code Online (Sandbox Code Playgroud)

更新的查询

然后我将查询更改为

EXPLAIN ANALYZE
SELECT `oc`.`oc_number` AS `ocNumber` , `roll`.`po_number` AS `poNumber` ,
`item`.`item_code` AS `itemCode` , `roll`.`roll_length` AS `rollLength` ,
`roll`.`roll_utilized` AS `rollUtilized`
FROM `fabric_barcode_rolls` AS `roll`
INNER JOIN `fabric_barcode_oc` AS `oc` ON `oc`.`oc_unique_id` = `roll`.`oc_unique_id`
INNER JOIN `fabric_barcode_items` AS `item` ON `item`.`item_unique_id` = `roll`.`item_unique_id_fk`
WHERE `roll`.`roll_number` = 'dZkzHJ_je8'
Run Code Online (Sandbox Code Playgroud)

这会生成以下执行计划

"-> Rows fetched before execution  (cost=0.00 rows=1) (actual time=0.000..0.000 rows=1 loops=1)
Run Code Online (Sandbox Code Playgroud)

这两个查询之间的唯一区别是我BINARY从查询中删除了函数调用。我很困惑为什么计划如此不同?

执行时间

查询 1 的执行时间约为 375 毫秒,而第二个查询的执行时间约为 160 毫秒。

是什么造成了这种差异?

更新

fabric_barcode_rolls根据要求包括表架构定义

fabric_barcode_rolls,"CREATE TABLE `fabric_barcode_rolls` (
  `roll_unique_id` int NOT NULL AUTO_INCREMENT,
  `oc_unique_id` int NOT NULL,
  `item_unique_id_fk` int NOT NULL,
  `roll_number` char(30) NOT NULL,
  `roll_length` decimal(10,2) DEFAULT '0.00',
  `po_number` char(22) DEFAULT NULL,
  `roll_utilized` decimal(10,2) DEFAULT '0.00',
  `user` char(30) NOT NULL,
  `mir_number` char(22) DEFAULT NULL,
  `mir_location` char(10) DEFAULT NULL,
  `mir_stamp` datetime DEFAULT NULL,
  `creation_stamp` datetime DEFAULT CURRENT_TIMESTAMP,
  `update_stamp` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`roll_unique_id`),
  UNIQUE KEY `roll_number` (`roll_number`),
  KEY `fabric_barcode_item_fk` (`item_unique_id_fk`),
  CONSTRAINT `fabric_barcode_item_fk` FOREIGN KEY (`item_unique_id_fk`) REFERENCES `fabric_barcode_items` (`item_unique_id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=610684 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci"
Run Code Online (Sandbox Code Playgroud)

O. *_*nes 7

您的性能差异是由于以下事实造成的:在 MySQL 中,VARCHAR() 和 CHAR() 列上的排序规则被烘焙到索引中。

编辑更新以匹配表定义。

您的fabric_barcode_rolls表有一列定义如下:

roll_number char(30) NOT NULL,
...
UNIQUE KEY roll_number (roll_number).
Run Code Online (Sandbox Code Playgroud)

因此,您的WHERE ... BINARY roll.roll_number = 'dZkzHJ_je8'过滤子句不可控制:它不能使用该列上的索引。但它WHERE ... roll.roll_number = 'dZkzHJ_je8'是可控制的:它确实使用索引。所以速度很快。但该列的默认排序规则不区分大小写。所以,它很快而且是错误的。

这是可以解决的。

请注意,该列上没有排序规则声明。这意味着它使用表的默认值:utf8mb4_0900_ai_ci,不区分大小写的排序规则。

对于普通条形码列,您需要的是每个字符一个字节的字符集和区分大小写的排序规则。这会改变你的桌子来做到这一点。

 ALTER TABLE fabric_barcode_rolls
CHANGE  roll_number 
        roll_number CHAR(30) COLLATE latin1_bin NOT NULL;
Run Code Online (Sandbox Code Playgroud)

这是多层次的胜利。使用正确的条形码字符集可以节省数据。它使索引更短并且使用更高效。它执行区分大小写(二进制匹配)的查找,这本身​​使索引更短且使用效率更高。而且它不会存在大小写字符集的条形码之间的冲突风险。

在您得出碰撞风险如此之低以至于不必担心之前,请阅读生日悖论。

  • 使用二进制排序规则也有一个好处,因为 MySQL 将整个字符串与“memcmp()”进行比较,而不是被迫逐个字符进行比较,以根据列的排序规则测试字符等效性。 (3认同)