【问题标题】:MariaDB: Using PK for inner query resultMariaDB:使用 PK 获取内部查询结果
【发布时间】:2022-01-20 10:29:49
【问题描述】:

我有三个表 (MariaDB 10.6.5):personprivate_personcorporate_person。在 person 中存储了 Id,在其他表中存储了名称,它们都连接到每个 FK 的 person

CREATE TABLE `person` (
    `Id` INT(11) NOT NULL AUTO_INCREMENT,
    `TypeOfPerson` ENUM('PRIVATE','CORPORATE') NOT NULL,
    PRIMARY KEY (`Id`)
) ENGINE=InnoDB;

CREATE TABLE `private_person` (
    `PersonId` INT(11) NOT NULL,
    `FirstName` VARCHAR(255) NULL DEFAULT NULL,
    `LastName` VARCHAR(255) NULL DEFAULT NULL,
    PRIMARY KEY (`PersonId`),
    INDEX `IX_private_person_FirstName` (`FirstName`),
    INDEX `IX_private_person_LastName` (`LastName`),
    CONSTRAINT `FK_private_person_person_PersonId` FOREIGN KEY (`PersonId`) REFERENCES `person` (`Id`) ON UPDATE RESTRICT ON DELETE RESTRICT
) ENGINE=InnoDB;

CREATE TABLE `corporate_person` (
    `PersonId` INT(11) NOT NULL,
    `Name` VARCHAR(255) NULL DEFAULT NULL,
    PRIMARY KEY (`PersonId`),
    INDEX `IX_corporate_person_Name` (`Name`),
    CONSTRAINT `FK_corporate_person_person_PersonId` FOREIGN KEY (`PersonId`) REFERENCES `person` (`Id`) ON UPDATE RESTRICT ON DELETE RESTRICT
) ENGINE=InnoDB;

现在我必须在 private_personcorporate_person 两个表中搜索名称:

SELECT `p`.Id
  FROM `test`.`person` AS `p`
  LEFT JOIN `test`.`private_person` AS `p0` ON `p`.`Id` = `p0`.`PersonId`
  LEFT JOIN `test`.`corporate_person` AS `c0` ON `p`.`Id` = `c0`.`PersonId`
  WHERE `p0`.`FirstName` = 'Test' OR p0.LastName = 'Test' OR `c0`.`Name` = 'Test';

但是查询有点慢,因为person中有很多行:

所以我改变了查询:

SELECT Id FROM `test`.`person` WHERE Id IN (
SELECT p.Id
  FROM `test`.`person` AS `p`
  INNER JOIN `test`.`private_person` AS `p0` ON `p`.`Id` = `p0`.`PersonId`
  WHERE `p0`.`FirstName` = 'Test' OR `p0`.`LastName` = 'Test'
UNION SELECT p.Id
  FROM `test`.`person` AS `p`
  INNER JOIN `test`.`corporate_person` AS `c0` ON `p`.`Id` = `c0`.`PersonId`
  WHERE `c0`.`Name` = 'Test' ORDER BY Id);

内部查询(UNION)非常快,但是,整个语句也很慢:

我不明白为什么。内部查询只给出一定数量的 Id,为什么优化器不对这些简单数量的 Id 使用主索引?当我给出 Id 而不是内部查询时

SELECT Id FROM `test`.`person` WHERE Id IN (25251, 47413, 99851 ...);

该语句当然也很快:

即使我强制使用主索引 (SELECT Id FROM test.person FORCE INDEX (PRIMARY) WHERE ...),它也不会改变任何东西;根据查询优化器,现在使用主索引,但语句并不快:

如果优化器仅从子查询中获取一定数量的 Id,为什么不以更快(更)的方式使用主索引?

编辑: 很抱歉造成一些误解。我不想让查询更快,实际上我之前有一个解决我的具体问题的解决方案(比这里描述的更复杂的场景中的慢查询),也许我错过了更明确地写这个。但是在开发一个语句的过程中我尝试使用我对mysql和优化器的知识,在这里我很惊讶,我不明白,问题出在mariadb上。同样,OUTER 语句只获取一组 id,并且不能以正确的方式使用 PK。在SELECT Id FROM tabA WHERE Id IN (123, 456, 789) 中使用了PK 并且查询非常快,但是SELECT Id FROM tabA WHERE Id IN (SELECT Id FROM tabB WHERE ...) 没有以正确的方式使用PK,优化器会爬取整个表tabA。为什么?这就是我想问的问题。

【问题讨论】:

  • 既然“人”都有“名字”,把name移到公用表!
  • 这不是我的桌子,我不能改变结构。

标签: mysql indexing mariadb subquery query-optimization


【解决方案1】:

OR 操作会破坏您的查询。

  WHERE `p0`.`FirstName` = 'Test' OR p0.LastName = 'Test' OR `c0`.`Name` = 'Test';

这是查询优化的一个常见问题,因为 MySQL 对每个表引用仅使用一个索引(即使存在 index merge optimization,但它并不像您想象的那样频繁出现)。

问题是优化器不能像这样使用单个 B 树索引来查找几个不同的列。

我经常使用的类比是电话簿。如果您想按姓氏查找一个人,电话簿的顺序可以帮助您高效地进行查找,因为它首先按姓氏的字母顺序排列。但是,如果您想按名字查找一个人,这本书没有帮助,因为条目不是按名字排序的。如果你想按姓名查找一个人,你仍然需要扫描整本书才能找到那些按名字匹配的人。

假设您有第二本按名字排序的电话簿。这会有所帮助,但是如果您受到一个规则的限制,该规则迫使您只使用一本书或另一本书而不是两者进行搜索,那么您就会陷入困境。无论您选择哪本书,您都必须扫描整本书以搜索另一个名字。

当优化器只允许每个表引用一个索引并且您的查询中有OR 条件时,就会发生这种情况。

许多人使用的解决方法是执行多个查询并UNION 查询结果。

SELECT `p`.Id
FROM `test`.`person` AS `p`
INNER JOIN `test`.`private_person` AS `p0` ON `p`.`Id` = `p0`.`PersonId`
WHERE `p0`.`FirstName` = 'Test'
UNION
SELECT `p`.Id
FROM `test`.`person` AS `p`
INNER JOIN `test`.`private_person` AS `p0` ON `p`.`Id` = `p0`.`PersonId`
WHERE p0.LastName = 'Test'
UNION
SELECT `p`.Id
FROM `test`.`person` AS `p`
INNER JOIN `test`.`corporate_person` AS `c0` ON `p`.`Id` = `c0`.`PersonId`
WHERE `c0`.`Name` = 'Test';

在这种情况下,每个表引用可以使用不同的索引,并更有效地搜索您想要的名称。一旦找到一小部分匹配的行,它们就会通过该表的主键连接回person 表(MySQL 知道如何以不同于您在查询中列出它们的顺序访问表,所以不要担心哪个表在您的联接中首先被命名)。


你的评论:

有时,优化器会根据其成本估算模型对如何排序表做出一些奇怪的决定,即首先访问哪个表更好。您可以像这样覆盖它:

SELECT `p`.Id
FROM `private_person` AS `p0` 
STRAIGHT_JOIN `person` AS `p` ON `p`.`Id` = `p0`.`PersonId`
WHERE `p0`.`FirstName` = 'Test'
UNION
SELECT `p`.Id
FROM `private_person` AS `p0`
STRAIGHT_JOIN `person` AS `p` ON `p`.`Id` = `p0`.`PersonId`
WHERE p0.LastName = 'Test'
UNION
SELECT `p`.Id
FROM `corporate_person` AS `c0` 
STRAIGHT_JOIN `person` AS `p` ON `p`.`Id` = `c0`.`PersonId`
WHERE `c0`.`Name` = 'Test';

STRAIGHT_JOIN 表示使用 SQL 查询中出现的表顺序。

有了这个,我测试了 EXPLAIN 输出并得到了这个:

*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: p0
   partitions: NULL
         type: ref
possible_keys: PRIMARY,IX_private_person_FirstName
          key: IX_private_person_FirstName
      key_len: 1023
          ref: const
         rows: 1
     filtered: 100.00
        Extra: Using index
*************************** 2. row ***************************
           id: 1
  select_type: PRIMARY
        table: p
   partitions: NULL
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: test2.p0.PersonId
         rows: 1
     filtered: 100.00
        Extra: Using index
*************************** 3. row ***************************
           id: 2
  select_type: UNION
        table: p0
   partitions: NULL
         type: ref
possible_keys: PRIMARY,IX_private_person_LastName
          key: IX_private_person_LastName
      key_len: 1023
          ref: const
         rows: 1
     filtered: 100.00
        Extra: Using index
*************************** 4. row ***************************
           id: 2
  select_type: UNION
        table: p
   partitions: NULL
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: test2.p0.PersonId
         rows: 1
     filtered: 100.00
        Extra: Using index
*************************** 5. row ***************************
           id: 3
  select_type: UNION
        table: c0
   partitions: NULL
         type: ref
possible_keys: PRIMARY,IX_corporate_person_Name
          key: IX_corporate_person_Name
      key_len: 1023
          ref: const
         rows: 1
     filtered: 100.00
        Extra: Using index
*************************** 6. row ***************************
           id: 3
  select_type: UNION
        table: p
   partitions: NULL
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: test2.c0.PersonId
         rows: 1
     filtered: 100.00
        Extra: Using index
*************************** 7. row ***************************
           id: NULL
  select_type: UNION RESULT
        table: <union1,2,3>
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: NULL
     filtered: NULL
        Extra: Using temporary

这看起来会更好地使用索引,并以更好的顺序访问表。

需要注意的是,我刚刚使用空表进行了测试,这有时会使优化器对如何计算成本感到困惑。在您的环境中尝试一下。

【讨论】:

  • 嗨比尔,谢谢你的解释!但是您解释了,为什么第一个语句很慢,但是我想知道,为什么更改后的语句也很慢?有一个UNION,INNER子查询非常快。您可以在(第二个)查询计划中看到它。只有外部语句 'person' 使查询变得如此缓慢,但为什么呢?此外部语句仅从内部语句中获取 Id,用于 PK。为什么要为一组 id 爬取整个表,存储在 PK 中?
  • 比尔,再次感谢您的精彩解释!你是对的,你的查询速度和预期的一样快,而且我学到了一些新东西,之前没有听说过STRAIGHT_JOIN。谢谢!但是,请参阅我上面的编辑。
【解决方案2】:

在您的 Union 情况下,SELECT Id FROM test.person WHERE Id IN 是不必要的。这可能会导致效率低下。

如果您需要的不仅仅是结果中的Id,请尝试以下操作:

SELECT ...  -- (more than just Id)
    FROM  ( (SELECT ...)
            UNION ALL
            (SELECT ...)
          ) AS x;

这基本上迫使它在考虑外部 Select 之前先进行 Union。

注意:UNION ALL 可能会导致重复(取决于细节),但比UNION(意味着UNION DISTINCT)更快。也就是说,如果您知道没有重复,请添加 ALL

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-04-19
    • 1970-01-01
    • 1970-01-01
    • 2019-07-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-06
    相关资源
    最近更新 更多