使用 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 |
我在所有必填字段上都有索引,即。
用户Table:索引uuid
主页Table:phoneNumberHash 和 secondaryPhoneNumberHash 上的单独索引
- Utility_Tags:tag_name 和 tag_value
上的单独索引
查询我是运行:
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)应该分别通过 phoneNumberHash
和 secondaryPhoneNumberHash
上的索引进行优化。该联合中的第三个查询需要 (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 个索引。
- Select来自子查询first,因此可以使用
users
中的索引
- 确保所有查找的列都有索引,即
(uuid)
、(phoneNumberHash)
、(secondaryPhoneNumberHash)
和 (tag_name, tag_value)
- 分解您的查询以根除
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-加入,这将提高性能,尤其是在有大量已删除用户的情况下。
问题涵盖了对多个子查询的高效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 |
我在所有必填字段上都有索引,即。
用户Table:索引uuid
主页Table:phoneNumberHash 和 secondaryPhoneNumberHash 上的单独索引
- Utility_Tags:tag_name 和 tag_value 上的单独索引
查询我是运行:
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)应该分别通过 phoneNumberHash
和 secondaryPhoneNumberHash
上的索引进行优化。该联合中的第三个查询需要 (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 个索引。
- Select来自子查询first,因此可以使用
users
中的索引 - 确保所有查找的列都有索引,即
(uuid)
、(phoneNumberHash)
、(secondaryPhoneNumberHash)
和(tag_name, tag_value)
- 分解您的查询以根除
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-加入,这将提高性能,尤其是在有大量已删除用户的情况下。