MySQL 中的慢速内部连接与子查询
Slow inner joins with subqueries in MySQL
我遇到了一个缓慢的 MySQL 查询问题 (MySQL 5+)。让我们想想三个 tables:
customers:
- id_customer : int (PRIMARY)
- name : varchar(255)
customers_addresses:
- id_customers_addresses : int (PRIMARY)
- id_customer : int (INDEX)
- street : varchar(255)
- zipcode : varchar(255)
- city : varchar(255)
customers_contacts:
- id_customers_contacts : int (PRIMARY)
- id_customer : int (INDEX)
- type : varchar(255)
- value : varchar(255)
现在,我的目标是在一个查询中收集所有地址和联系信息,每个客户一行。我的第一次尝试是使用 LEFT JOIN
s,因为一些客户没有任何地址 and/or 联系信息:
SELECT customers.id_customer,
customers.name,
X.contact AS contact,
Y.street,
Y.zipcode,
Y.city
FROM customers
LEFT JOIN
(
SELECT
GROUP_CONCAT( CONCAT( type, ': ', value ) SEPARATOR ', ' ) AS contact,
id_customer
FROM customers_contacts
GROUP BY id_customer
) AS X
ON X.id_customer = customers.id_customer
LEFT JOIN
(
SELECT
GROUP_CONCAT(street SEPARATOR '<br>' ) AS street,
GROUP_CONCAT(zipcode SEPARATOR '<br>' ) AS zipcode,
GROUP_CONCAT(city SEPARATOR '<br>' ) AS city,
id_customer
FROM customers_addresses
GROUP BY id_customer
) AS Y
ON Y.id_customer = customers.id_customer
WHERE Y.street LIKE '%Avenue%'
ORDER BY customers.name DESC
LIMIT 0, 20
此查询用了 130 多秒才完成(每个 table 约 7000 个条目),这很不理想。
前置 EXPLAIN EXTENDED
给出:
id select_type table type possible_keys key key_len ref rows filtered Extra
1 PRIMARY customers ref name name 3 const 4334 100.00 Using where; Using temporary; Using filesort
1 PRIMARY <derived2> ALL NULL NULL NULL NULL 7793 100.00
1 PRIMARY <derived3> ALL NULL NULL NULL NULL 8580 100.00 Using where
3 DERIVED customers_addresses index NULL id_customer 5 NULL 8651 100.00
2 DERIVED customers_contacts index NULL id_customer 4 NULL 9314 100.00
我阅读了一些 Whosebug 帖子和 MySQL 文档。两者都说 INNER JOIN
快得多。我试图通过使用 UNION ALL
:
来复制 LEFT JOIN
与 INNER JOIN
的行为
SELECT customers.id_customer,
customers.name,
X.contact AS contact,
Y.street,
Y.zipcode,
Y.city
FROM customers
INNER JOIN
(
SELECT
GROUP_CONCAT( CONCAT( type, ': ', value ) SEPARATOR ', ' ) AS contact,
id_customer
FROM customers_contacts
GROUP BY id_customer
UNION ALL
SELECT
'' AS contact,
id_customer
FROM customers
WHERE id_customer NOT IN (SELECT DISTINCT id_customer FROM customers_contacts)
) AS X
ON X.id_customer = customers.id_customer
INNER JOIN
(
SELECT
GROUP_CONCAT(street SEPARATOR '<br>' ) AS street,
GROUP_CONCAT(zipcode SEPARATOR '<br>' ) AS zipcode,
GROUP_CONCAT(city SEPARATOR '<br>' ) AS city,
id_customer
FROM customers_addresses
GROUP BY id_customer
UNION ALL
SELECT
'' AS street,
'' AS zipcode,
'' AS city,
id_customer
FROM customers
WHERE id_customer NOT IN (SELECT DISTINCT id_customer FROM customers_addresses)
) AS Y
ON Y.id_customer = customers.id_customer
WHERE Y.street LIKE '%Avenue%'
ORDER BY customers.name DESC
LIMIT 0, 20
此查询将性能提高了 20 秒。但是110秒还是不行table.
前置 EXPLAIN EXTENDED
:
id select_type table type possible_keys key key_len ref rows filtered Extra
1 PRIMARY <derived2> ALL NULL NULL NULL NULL 8596 100.00 Using temporary; Using filesort
1 PRIMARY <derived5> ALL NULL NULL NULL NULL 8604 100.00 Using join buffer
1 PRIMARY customers eq_ref PRIMARY,name,name3 PRIMARY 4 Y.id_kunde 1 100.00 Using where
5 DERIVED customers_addresses index NULL id_kunde 5 NULL 8651 100.00
6 UNION customers index NULL name2 767 NULL 8677 100.00 Using where; Using index
7 DEPENDENT SUBQUERY customers_addresses index_subquery id_kunde id_kunde 5 func 2 100.00 Using index
NULL UNION RESULT <union5,6> ALL NULL NULL NULL NULL NULL NULL
2 DERIVED customers_contacts index NULL id_kunde 4 NULL 10411 100.00
3 UNION customers index NULL name2 767 NULL 8677 100.00 Using where; Using index
4 DEPENDENT SUBQUERY customers_contacts index_subquery id_kunde id_kunde 4 func 1 100.00 Using index
NULL UNION RESULT <union2,3> ALL NULL NULL NULL NULL NULL NULL
所以这是我的问题:如何改进这些查询之一 and/or 数据库 tables 以获得超快速响应?我不仅对解决方案感兴趣,而且对将来如何防止此类性能下降的策略感兴趣。
此致。
作为此处适用的一般规则,您可以说以下内容:
每当您使用连接 select 结果(子查询)的查询时,MySQL 必须先 运行 这些子查询,然后从 table结果。你这样做了两次,这意味着 MySQL 首先创建 2 tables,只有在结果完成后才删除它们。通过 MySQL 的适当内存管理,这是在内存中完成的。但是这些 table 是在没有索引的情况下创建的,因为 MySQL 无法神奇地确定哪个索引最适合这些派生的 table,并且因为它们通常是在内存中创建的,所以查询这些非常快(不如使用键的 SELECT 快)。
然后,当两个 table 完成后,MySQL 必须将您原来的 table 与两者结合起来,并即时创建第三个 table需要根据您的条件进行过滤和排序。
这是一个性能杀手。您的要求存在的问题之一是每个客户只能产生一行。这不是数据库保存信息的方式,因此您在 运行 时间内为数据转换(您的 GROUP_CONCAT 语句)付出了代价。我不是 100% 确定当前的 MySQL 数据库引擎对 UNION 语句做了什么,所以我不想对这些发表评论。
在可用键上使用简单的 INNER JOIN,但当结果是多个地址时,会为一个客户生成多行,您会发现性能大幅提升。您可以轻松地遍历编程语言层中的客户,一次请求一个客户的地址,如果您不愿意table 将所有客户和所有关联地址的结果拆分为该层上的客户。
TL;DR:放弃您的要求或忍受开销。
我遇到了一个缓慢的 MySQL 查询问题 (MySQL 5+)。让我们想想三个 tables:
customers:
- id_customer : int (PRIMARY)
- name : varchar(255)
customers_addresses:
- id_customers_addresses : int (PRIMARY)
- id_customer : int (INDEX)
- street : varchar(255)
- zipcode : varchar(255)
- city : varchar(255)
customers_contacts:
- id_customers_contacts : int (PRIMARY)
- id_customer : int (INDEX)
- type : varchar(255)
- value : varchar(255)
现在,我的目标是在一个查询中收集所有地址和联系信息,每个客户一行。我的第一次尝试是使用 LEFT JOIN
s,因为一些客户没有任何地址 and/or 联系信息:
SELECT customers.id_customer,
customers.name,
X.contact AS contact,
Y.street,
Y.zipcode,
Y.city
FROM customers
LEFT JOIN
(
SELECT
GROUP_CONCAT( CONCAT( type, ': ', value ) SEPARATOR ', ' ) AS contact,
id_customer
FROM customers_contacts
GROUP BY id_customer
) AS X
ON X.id_customer = customers.id_customer
LEFT JOIN
(
SELECT
GROUP_CONCAT(street SEPARATOR '<br>' ) AS street,
GROUP_CONCAT(zipcode SEPARATOR '<br>' ) AS zipcode,
GROUP_CONCAT(city SEPARATOR '<br>' ) AS city,
id_customer
FROM customers_addresses
GROUP BY id_customer
) AS Y
ON Y.id_customer = customers.id_customer
WHERE Y.street LIKE '%Avenue%'
ORDER BY customers.name DESC
LIMIT 0, 20
此查询用了 130 多秒才完成(每个 table 约 7000 个条目),这很不理想。
前置 EXPLAIN EXTENDED
给出:
id select_type table type possible_keys key key_len ref rows filtered Extra
1 PRIMARY customers ref name name 3 const 4334 100.00 Using where; Using temporary; Using filesort
1 PRIMARY <derived2> ALL NULL NULL NULL NULL 7793 100.00
1 PRIMARY <derived3> ALL NULL NULL NULL NULL 8580 100.00 Using where
3 DERIVED customers_addresses index NULL id_customer 5 NULL 8651 100.00
2 DERIVED customers_contacts index NULL id_customer 4 NULL 9314 100.00
我阅读了一些 Whosebug 帖子和 MySQL 文档。两者都说 INNER JOIN
快得多。我试图通过使用 UNION ALL
:
LEFT JOIN
与 INNER JOIN
的行为
SELECT customers.id_customer,
customers.name,
X.contact AS contact,
Y.street,
Y.zipcode,
Y.city
FROM customers
INNER JOIN
(
SELECT
GROUP_CONCAT( CONCAT( type, ': ', value ) SEPARATOR ', ' ) AS contact,
id_customer
FROM customers_contacts
GROUP BY id_customer
UNION ALL
SELECT
'' AS contact,
id_customer
FROM customers
WHERE id_customer NOT IN (SELECT DISTINCT id_customer FROM customers_contacts)
) AS X
ON X.id_customer = customers.id_customer
INNER JOIN
(
SELECT
GROUP_CONCAT(street SEPARATOR '<br>' ) AS street,
GROUP_CONCAT(zipcode SEPARATOR '<br>' ) AS zipcode,
GROUP_CONCAT(city SEPARATOR '<br>' ) AS city,
id_customer
FROM customers_addresses
GROUP BY id_customer
UNION ALL
SELECT
'' AS street,
'' AS zipcode,
'' AS city,
id_customer
FROM customers
WHERE id_customer NOT IN (SELECT DISTINCT id_customer FROM customers_addresses)
) AS Y
ON Y.id_customer = customers.id_customer
WHERE Y.street LIKE '%Avenue%'
ORDER BY customers.name DESC
LIMIT 0, 20
此查询将性能提高了 20 秒。但是110秒还是不行table.
前置 EXPLAIN EXTENDED
:
id select_type table type possible_keys key key_len ref rows filtered Extra
1 PRIMARY <derived2> ALL NULL NULL NULL NULL 8596 100.00 Using temporary; Using filesort
1 PRIMARY <derived5> ALL NULL NULL NULL NULL 8604 100.00 Using join buffer
1 PRIMARY customers eq_ref PRIMARY,name,name3 PRIMARY 4 Y.id_kunde 1 100.00 Using where
5 DERIVED customers_addresses index NULL id_kunde 5 NULL 8651 100.00
6 UNION customers index NULL name2 767 NULL 8677 100.00 Using where; Using index
7 DEPENDENT SUBQUERY customers_addresses index_subquery id_kunde id_kunde 5 func 2 100.00 Using index
NULL UNION RESULT <union5,6> ALL NULL NULL NULL NULL NULL NULL
2 DERIVED customers_contacts index NULL id_kunde 4 NULL 10411 100.00
3 UNION customers index NULL name2 767 NULL 8677 100.00 Using where; Using index
4 DEPENDENT SUBQUERY customers_contacts index_subquery id_kunde id_kunde 4 func 1 100.00 Using index
NULL UNION RESULT <union2,3> ALL NULL NULL NULL NULL NULL NULL
所以这是我的问题:如何改进这些查询之一 and/or 数据库 tables 以获得超快速响应?我不仅对解决方案感兴趣,而且对将来如何防止此类性能下降的策略感兴趣。
此致。
作为此处适用的一般规则,您可以说以下内容:
每当您使用连接 select 结果(子查询)的查询时,MySQL 必须先 运行 这些子查询,然后从 table结果。你这样做了两次,这意味着 MySQL 首先创建 2 tables,只有在结果完成后才删除它们。通过 MySQL 的适当内存管理,这是在内存中完成的。但是这些 table 是在没有索引的情况下创建的,因为 MySQL 无法神奇地确定哪个索引最适合这些派生的 table,并且因为它们通常是在内存中创建的,所以查询这些非常快(不如使用键的 SELECT 快)。
然后,当两个 table 完成后,MySQL 必须将您原来的 table 与两者结合起来,并即时创建第三个 table需要根据您的条件进行过滤和排序。
这是一个性能杀手。您的要求存在的问题之一是每个客户只能产生一行。这不是数据库保存信息的方式,因此您在 运行 时间内为数据转换(您的 GROUP_CONCAT 语句)付出了代价。我不是 100% 确定当前的 MySQL 数据库引擎对 UNION 语句做了什么,所以我不想对这些发表评论。
在可用键上使用简单的 INNER JOIN,但当结果是多个地址时,会为一个客户生成多行,您会发现性能大幅提升。您可以轻松地遍历编程语言层中的客户,一次请求一个客户的地址,如果您不愿意table 将所有客户和所有关联地址的结果拆分为该层上的客户。
TL;DR:放弃您的要求或忍受开销。