MySQL 通过使用连接查询来优化联合查询
Posted
技术标签:
【中文标题】MySQL 通过使用连接查询来优化联合查询【英文标题】:MySQL optimize a union-query by using a join-query instead 【发布时间】:2016-11-01 21:25:16 【问题描述】:我有 3 张表 - 一张用于用户,一张用于收款,一张用于付款。我想在一个结果集中显示所有收款和付款。我可以用多个select
s 和一个union
来做到这一点,但它看起来很麻烦,而且我怀疑由于子查询而速度很慢——而且表非常大(尽管我正在使用索引)。有没有更快的方法来实现这一目标?也许使用full outer join
?
这是带有一些示例数据的架构的简化版本:
create table users (
id int auto_increment,
name varchar(20),
primary key (id)
) engine=InnoDB;
insert into users (name) values ('bob'),('fred');
create table user_incoming_payments (
user_id int,
funds_in int
) engine=InnoDB;
insert into user_incoming_payments
values (1,100),(1,101),(1,102),(1,103),
(2,200),(2,201),(2,202),(2,203);
create table user_outgoing_payments (
user_id int,
funds_out int
) engine=InnoDB;
insert into user_outgoing_payments
values (1,100),(1,101),(2,200),(2,201);
这是一个丑陋的查询,它为用户 bob 生成了我想要的结果:
select * from (
(select u.name, i.funds_in, 0 as 'funds_out' from users u
inner join user_incoming_payments i on u.id = i.user_id)
union
(select u.name, 0 as 'funds_in', o.funds_out from users u
inner join user_outgoing_payments o on u.id = o.user_id)
) a where a.name = 'bob'
order by a.funds_in asc, a.funds_out asc;
这是我可以用join
s 做同样的事情的最接近的地方——虽然它不正确,因为我希望这个结果集看起来和以前一样,我不知道如何使用@ 987654328@:
select *
from users u
right join user_incoming_payments i on u.id = i.user_id
right join user_outgoing_payments o on u.id = o.user_id
where u.name = 'bob';
SQL Fiddle here
【问题讨论】:
在 mysql 中没有 FULL OUTER JOIN 之类的东西 - 尽管显然您可以通过各种方式模拟一个。但是,如果该查询生成了所需的结果,那么 FULL OUTER JOIN 就不是我们想要的了。您的查询很好(虽然我不确定超级查询是否必要) @Ivan:您看不到问题中已经存在的示例数据和结果?如果没有,你没有读得很透。 最好将过滤器where u.name = 'bob'
放在子查询中。否则它将生成一个中间表,其中所有用户都加入了他们的付款,然后它必须找到其中的所有鲍勃。
但除此之外,您的联合方法是正确的方法。
我同意巴马尔的观点。 MySQL 不会将外部查询上的谓词name = 'bob'
“推”到内联视图中。 MySQL 将实现一个派生表,其中包含bob
和fred
的行。在最新版本的 MySQL 之前,派生表上没有索引。并且不需要派生表。 Strawberry 是对的(除了查询应该使用 UNION ALL
而不是 UNION
),除非我们遗漏了“删除重复行”的部分规范。
【参考方案1】:
MySQL 不支持FULL OUTER JOIN
。即使它确实支持它,我不认为你会想要它,因为它会引入一个半笛卡尔积......来自incoming_
的每一行匹配outgoing_
中的每一行,创建额外的行。
如果incoming_
有四行,outgoing_
有六行,则连接操作生成的集合将包含 24 行。
这看起来更像是你想要一个集合连接操作。也就是说,您有两个单独的集合要连接在一起。这不是JOIN
操作。这是 UNION ALL
设置操作。
SELECT ... FROM ...
UNION ALL
SELECT ... FROM ...
如果您不需要删除重复项(而且在这种情况下您似乎不想这样做,如果 incoming_
中有多个行具有相同的 funds_in
值,我不认为您想删除任何行。)...
然后使用UNION ALL
set 运算符,它不执行重复行的检查和删除。
UNION
运算符删除重复行。哪个(再次)我不认为你想要。
派生表不是必需的。
并且 MySQL 不会将谓词从外部表“推送”到内联视图中。这意味着 MySQL 将物化一个派生表,其中包含 all 用户的所有传入和传出。外部查询将通过它查找行。在最新版本的 MySQL 之前,没有在派生表上创建索引。
有关更有效查询的示例,请参阅 Strawberry 的答案。
对于小示例集,索引不会产生任何影响。但是,对于一个大集合,您将需要添加适当的覆盖索引。
此外,对于这样的查询,我倾向于包含一个鉴别器列,告诉我哪个查询返回了一行。
(
SELECT 'i' AS src
, ...
FROM ...
)
UNION ALL
(
SELECT 'o' AS src
, ...
FROM ...
)
ORDER BY ...
【讨论】:
他不会得到重复的行,因为他将funds_in
和funds_out
放在子查询的不同列中。
@Barmar:但是如果来自incoming_
的两行具有相同的user_id
和funds_in
值。示例数据没有显示任何重复...但是如果我们在incoming_
表(1,100),(1,100),(1,100)
中插入更多行会怎样。我们没有看到任何阻止这种情况的约束。请注意,UNION
操作会从组合集中删除 all 重复项。行来自哪个集合无关紧要...扫描整个组合集合以查找重复项。
好点。我怀疑在实际应用程序中还有一个事务时间可以区分它们,但是使用我们提供的架构是正确的。
@Barmar:是的。如果这是一家银行/信用合作社,那么ingoing_
和outgoing_
的每一行都可能有一个唯一的交易 ID。我知道我不希望我的信用合作社“崩溃”我的存款,因为它们是重复的金额。即使我们保证不会有重复,我仍然会使用UNION ALL
,只是为了避免检查重复所需的排序开销。
确实,我想我自己就是因为这个原因才这样做的。【参考方案2】:
使用此模型,我可能会按如下方式编写该查询,但我怀疑它是否有很大不同...
select u.name
, i.funds_in
, 0 funds_out
from users u
join user_incoming_payments i
on u.id = i.user_id
where u.name = 'bob'
union all
select u.name
, 0 funds_in
, o.funds_out
from users u
join user_outgoing_payments o
on u.id = o.user_id
where u.name = 'bob'
order
by funds_in asc
, funds_out asc;
但是,请注意这里没有 PK,这可能会出现问题。
如果是我,我会有一个交易表,其中包括一个 transaction_id PK、每笔交易的时间戳,以及一个用于记录值是贷方还是借方的列。
【讨论】:
请注意,UNION
将删除重复项,这在这种情况下可能是不可取的。我们看不到任何保证user_incoming_payments
中不会有两个(或更多)行具有相同的funds_in
值。我们可能想使用UNION ALL
集合运算符来避免删除重复项。 (如果不需要删除重复项,我们更喜欢使用UNION ALL
来提高性能,以避免检查重复项的开销。
另外,对于这种类型的查询,我通常包括一个鉴别列,即每个查询中的一个额外列,每个查询返回一个不同的短文字值(可能是'i'
和@在这种情况下为 987654328@,这让我知道哪个查询返回了一行。
@spencer7593 已修复(带有警告)以上是关于MySQL 通过使用连接查询来优化联合查询的主要内容,如果未能解决你的问题,请参考以下文章