派生表中的MySQL联合(related_id = a AND related_id = b)OR(related_id = z)

时间:2014-03-03 22:33:42

标签: mysql inner-join union derived-table

我有以下表格:userstagstags_data tags_data包含tag_iduser_id列,用于将userstags链接为1个用户与多个代码的关系。

列出具有tag_id 1001 AND 1003, OR tag_id 1004的所有用户的最佳方式是什么? 编辑:我的意思是,只要肯定有1004 OR(1001和1003),也可以有其他相关的标签。

目前我有两种方法可以做到这一点,在UNION子句或FROM子句中使用派生表中的INNER JOIN ...

SELECT subsel.user_id, users.name 
FROM   ( SELECT user_id 
         FROM   tags_data
         WHERE  tag_id IN (1001, 1003) 
         GROUP  BY user_id 
         HAVING COUNT(tag_id)=2
        UNION 
         SELECT user_id 
         FROM   tags_data 
         WHERE  tag_id=1004
       ) AS subsel 
LEFT JOIN users ON subsel.user_id=users.user_id

或者

SELECT users.user_id, users.name
FROM   users
INNER JOIN ( SELECT user_id
             FROM   tags_data
             WHERE  tag_id  IN (1001, 1003) 
             GROUP  BY user_id
             HAVING COUNT(tag_id)=2
            UNION 
             SELECT user_id
             FROM   tags_data
             WHERE  tag_id=1004
           ) AS subsel ON users.user_id=subsel.user_id

我还有LEFT JOIN其他表格。 users表中的50k +行和tags_data表中的150k +行。

这是一个将数据导出到另一个系统的批处理作业,因此不是最终用户运行的实时查询,因此性能不是非常关键。但是我想尽力获得最好的结果。派生表的查询实际上应该非常快,在我将更多连接,函数和计算字段添加到返回给客户端的结果之前,缩小结果集的范围是有意义的。稍后我将在更大的数据集上运行这些数据集以查看是否存在任何性能差异,但运行EXPLAIN会显示几乎相同的执行计划。

除非绝对必要,否则我会尽量避免使用UNIONs。但我认为在这种情况下,由于两个有效无关的标准,我几乎必须在某处定义UNION

我可以在这里使用另一种方法吗? 对于这类问题,是否存在某种特定的数据库术语?

完整示例架构:

CREATE TABLE IF NOT EXISTS `tags` (
  `tag_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `tag_name` varchar(255) NOT NULL,
  PRIMARY KEY (`tag_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1006 ;

INSERT INTO `tags` (`tag_id`, `tag_name`) VALUES
(1001, 'tag1001'),
(1002, 'tag1002'),
(1003, 'tag1003'),
(1004, 'tag1004'),
(1005, 'tag1005');

CREATE TABLE IF NOT EXISTS `tags_data` (
  `tags_data_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `tag_id` int(11) NOT NULL,
  PRIMARY KEY (`tags_data_id`),
  KEY `user_id` (`user_id`,`tag_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=11 ;

INSERT INTO `tags_data` (`tags_data_id`, `user_id`, `tag_id`) VALUES
(1, 1, 1001),
(2, 1, 1002),
(3, 1, 1003),
(4, 5, 1001),
(5, 5, 1003),
(6, 5, 1005),
(7, 8, 1004),
(8, 9, 1001),
(9, 9, 1002),
(10, 9, 1004);

CREATE TABLE IF NOT EXISTS `users` (
  `user_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=11 ;

INSERT INTO `users` (`user_id`, `name`) VALUES
(1, 'user1'),
(2, 'user2'),
(3, 'user3'),
(4, 'user4'),
(5, 'user5'),
(6, 'user6'),
(7, 'user7'),
(8, 'user8'),
(9, 'user9'),
(10, 'user10');

3 个答案:

答案 0 :(得分:1)

如果您正在寻找MySQL上的性能,您绝对应该避免使用嵌套查询和联合 - 其中大多数会导致临时表创建和扫描而没有索引。有一些罕见的例子,派生的临时表仍然使用索引,并且仅适用于某些特定情况和MySQL发行版。

我的建议是仅将查询重写为内部/外部联接,如下所示:

select distinct u.* from users as u 
  left outer join tags_data as t on 
    t.user_id=u.user_id and t.tag_id=1003 
  inner join tags_data as t2 on 
    t2.user_id=u.user_id 
    and (t2.tag_id=1004 or (t2.tag_id=1001 and t.tag_id=1003));

如果您可以确定没有用户同时拥有1004和(1001和1003)标签,您也可以删除" distinct"来自此查询,这将避免临时表创建。

您还应该使用索引,例如:

create index tags_data__user_id__idx on tags_data(user_id);
create index tags_data__tag_id__idx on tags_data(tag_id);

这样可以很容易地查询150k +结果集。

答案 1 :(得分:0)

使用内部查询将每个用户的所有标记分组为一个值,然后在where子句中使用简单过滤器:

select u.*
from users u
join (
  select user_id, group_concat(tag_id order by tag_id) tags
  from tags_data
  group by user_id
) t on t.user_id = u.user_id
where tags rlike '1001.*1003|1004'
此查询的

See SQLFiddle针对您的示例数据运行。

如果许多标记位于其中,您可以将where tag_id in (1001, 1003, 1004)添加到内部查询中,以将标记列表的大小减小为小优化。测试将显示这是否有很大不同。

这应该表现得非常好,因为每个表只扫描一次。

答案 2 :(得分:0)

高效,但不优雅,而且根本不灵活:

SELECT users.*
FROM users
LEFT JOIN tags_data AS tag1001
    ON (tag1001.user_id = users.user_id AND tag1001.tag_id = 1001)
LEFT JOIN tags_data AS tag1003
    ON (tag1003.user_id = users.user_id AND tag1003.tag_id = 1003)
LEFT JOIN tags_data AS tag1004
    ON (tag1004.user_id = users.user_id AND tag1004.tag_id = 1004)
WHERE (tag1001.tag_id AND tag1003.tag_id) OR (tag1004.tag_id);