MySQL 通过使用连接查询来优化联合查询

Posted

技术标签:

【中文标题】MySQL 通过使用连接查询来优化联合查询【英文标题】:MySQL optimize a union-query by using a join-query instead 【发布时间】:2016-11-01 21:25:16 【问题描述】:

我有 3 张表 - 一张用于用户,一张用于收款,一张用于付款。我想在一个结果集中显示所有收款和付款。我可以用多个selects 和一个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;

这是我可以用joins 做同样的事情的最接近的地方——虽然它不正确,因为我希望这个结果集看起来和以前一样,我不知道如何使用@ 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 将实现一个派生表,其中包含bobfred 的行。在最新版本的 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_infunds_out 放在子查询的不同列中。 @Barmar:但是如果来自incoming_ 的两行具有相同的user_idfunds_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 通过使用连接查询来优化联合查询的主要内容,如果未能解决你的问题,请参考以下文章

MySQL数据库联合查询与连接查询

MySQL增删改查之多表联合查询

mysql连接查询,子查询,联合查询

MySQL进阶 — 联合查询(外连接内连接子连接合并查询)

MySQL进阶 — 联合查询(外连接内连接子连接合并查询)

MySQL进阶 — 联合查询(外连接内连接子连接合并查询)