使用 OR 和 ORDER BY/LIMIT 来自多个表的高效多个子查询

Efficient multiple subqueries from multiple tables with OR and ORDER BY/LIMIT

问题涵盖了对多个子查询的高效SQL查询的疑惑:

我有 3 个 table。我想根据从 table 2 和 table 3 完成的过滤从 table 1 获取详细信息。目前我在 table 2 和 [=62= 上使用 IN 子句] 3 但 2M 用户大约需要 6 秒。我也尝试加入,但它比子查询慢。

Table1:

mysql>描述用户;

  Field                | Type             | Null | Key | Default   
| uuid                 | varchar(36)      | NO   | PRI | NULL  
| firstname            | varchar(512)     | YES  |     | NULL 
| status               | varchar(512)     | YES  |     | NULL 
| createdAt            | timestamp        | YES  |     | CURRENT_TIMESTAMP 

Table 2:

描述房屋;

| Field                    | Type             | Null | Key | Default           | Extra
| uuid                     | varchar(50)      | NO   | PRI | NULL 
| phoneNumberHash          | varchar(512)     | YES  | MUL | NULL 
| secondaryPhoneNumberHash | varchar(512)     | YES  | MUL | NULL  

Table 3:

描述 utility_tags:

| Field      | Type        | Null | Key | Default | 
| tag_name   | varchar(50) | NO   | MUL | NULL    |
| tag_value  | varchar(50) | NO   | MUL | NULL    | 
| user_id    | varchar(50) | NO   | MUL | NULL    | 

我在所有必填字段上都有索引,即。

查询我是运行:

SELECT uuid, firstname 
FROM users 
WHERE ( uuid in (
   SELECT `uuid` 
   FROM `homes` 
   WHERE ( ( `phoneNumberHash` = '02c' OR `secondaryPhoneNumberHash` = '02c' ))
 ) 
 OR uuid in (
   SELECT `user_id` 
   FROM `utility_tags`  
   WHERE  ( `tag_name` = 'ACCOUNT_NUMBER' AND `tag_value`= '13' )
 )) 
 AND `status` != 'DELETED' 
 ORDER BY `createdAt` DESC LIMIT 10 OFFSET 0;

当用户和家庭中有 2M 行时,查询很慢,大约需要 6 秒 table。

我尝试加入查询:

SELECT users.uuid, firstname 
FROM users inner join homes  on homes.uuid=users.uuid 
inner join utility_tags on utility_tags.user_id=users.uuid 
WHERE  ( phoneNumberHash = '02c' OR secondaryPhoneNumberHash = '02cd0' ) 
   OR  ( tag_name = 'ACCOUNT_NUMBER' AND tag_value= '1311851988' ) 
AND `status` != 'DELETED' 
ORDER BY `createdAt` DESC
LIMIT 10 OFFSET 0;

这大约需要 30 秒。

非常感谢任何帮助。

您正在根据其他 table 中的匹配项从 users table 中选择某些行。您为此使用了复杂的 IN( ... ) 子句。

让我们看看该子句的内容以获得优化可能性。这是生成一组 uuid 值的一种方法。

SELECT uuid 
  FROM homes 
 WHERE phoneNumberHash = '02c' 
    OR secondaryPhoneNumberHash = '02c'

这是另一个

 SELECT user_id 
   FROM utility_tags  
  WHERE tag_name = 'ACCOUNT_NUMBER' 
   AND tag_value= '13'

让我们将所有这些重铸为 UNION 几组 uuid 值,就像这样。

             SELECT uuid FROM homes WHERE phoneNumberHash = '02c'
             UNION 
             SELECT uuid FROM homes WHERE secondaryPhoneNumberHash = '02c'
             UNION 
             SELECT user_id AS uuid
               FROM utility_tags
              WHERE tag_name = 'ACCOUNT_NUMBER' 
                AND tag_value= '13'

三个查询的并集与所有 OR 子句的作用相同。这些查询中的前两个(如果您使用的是 InnoDB)应该分别通过 phoneNumberHashsecondaryPhoneNumberHash 上的索引进行优化。该联合中的第三个查询需要 (tag_name, tag_value, user_id) 上的复合索引才能高效执行。

UNION 最酷的地方在于它执行与 OR 相同的集合创建,但允许您在 UNION 中编写更可能使用索引的查询。我建议您尝试使用此 UNION 查询和适当的索引,直到您对其性能感到满意为止。然后你可以在你的外部查询中使用它。

(查询规划器可能已经变得足够聪明,可以单独将 phoneNumberHash = '02c' OR secondaryPhoneNumberHash = '02c' 作为 UNION 处理,一个接一个地利用您的两个索引。最近的 MySQL 版本取得了很大进展在查询计划中。)

这样就剩下了外部查询:

SELECT uuid, firstname 
  FROM users 
 WHERE matching uuids
   AND status != 'DELETED' 
 ORDER BY createdAt DESC
 LIMIT 10 OFFSET 0

这很难做到 sargable。查询规划器不喜欢 != 运算符。它最喜欢 = 因为索引相等扫描很便宜。它喜欢 <<=>=> 好的,因为范围扫描几乎同样便宜。但是你被 !=.

困住了

此外,查询规划器 讨厌 ORDER BY ... LIMIT 因为它必须对一大堆行进行排序,只是为了丢弃除了一小部分以外的所有行。

以下复合覆盖索引可以优化此查询:(createdAt, status, uuid, firstname)。如果查询规划器具有同时提供匹配条件和所需结果的索引,则它可能能够避开单独的 ORDER BY。也有可能这个指数会更好。 (createdAt, status, uuid, status, firstname) 你需要两个都试试。不要两者都保留,只保留最有帮助的一个。

综合起来:

SELECT u.uuid, u.firstname 
  FROM users u 
  JOIN (
             SELECT uuid FROM homes WHERE phoneNumberHash = '02c'
             UNION 
             SELECT uuid FROM homes WHERE secondaryPhoneNumberHash = '02c'
             UNION 
             SELECT user_id AS uuid
               FROM utility_tags
              WHERE tag_name = 'ACCOUNT_NUMBER' 
                AND tag_value= '13'
       ) s ON s.uuid = u.uuid
 WHERE status != 'DELETED' 
 ORDER BY createdAt DESC
 LIMIT 10 OFFSET 0

当您需要亚秒级查询响应时,megarow tables 上的事情会变得有趣。 http://use-the-index-luke.com/ 是这方面的一个很好的参考。

您的主要问题是您要从 users first 中进行选择 - 将其移至最后以便可以使用其索引(无法为子查询编制索引)。

此外,SQL OR 是臭名昭著的,主要是因为(几乎总是)最多可以使用 1 个索引。

  1. Select来自子查询first,因此可以使用users中的索引
  2. 确保所有查找的列都有索引,即 (uuid)(phoneNumberHash)(secondaryPhoneNumberHash)(tag_name, tag_value)
  3. 分解您的查询以根除 OR

试试这个:

SELECT uuid, firstname 
FROM (
    SELECT uuid
    FROM homes
    WHERE phoneNumberHash = '02c'
    UNION
    SELECT uuid
    FROM homes
    WHERE secondaryPhoneNumberHash = '02c'
    SELECT user_id 
    FROM utility_tags 
    WHERE tag_name = 'ACCOUNT_NUMBER'
    AND tag_value = 13
) x
JOIN users ON users.uuid = x.uuid
   AND status != 'DELETED' 
ORDER BY createdAt DESC
LIMIT 10 OFFSET 0

还要注意 status != 'DELETED' 的测试是在 join 条件中(而不是 WHERE 子句),所以它是在连接时执行的,而不是post-加入,这将提高性能,尤其是在有大量已删除用户的情况下。