从两个大表的连接中选择不同的值

Posted

技术标签:

【中文标题】从两个大表的连接中选择不同的值【英文标题】:Selecting distinct values from a join of two large tables 【发布时间】:2019-10-22 12:46:54 【问题描述】:

我有一个包含大约 300 万条记录的 animals 表。除了其他几列之外,该表还有一个idnameowner_id 列。我有一个 animal_breeds 表,大约有 250 万条记录。该表只有animal_idbreed 列。

我正在尝试查找与特定 owner_id 关联的不同 breed 值,但查询需要 20 秒左右。这是查询:

SELECT DISTINCT `breed`
FROM `animal_breeds` 
INNER JOIN `animals` ON `animals`.`id` = `animal_breeds`.`animal_id` 
WHERE `animals`.`owner_id` = ? ;

这些表具有所有适当的索引。我无法通过将breed 列添加到animals 表来对表进行非规范化,因为可以为动物分配多个品种。我对其他一些具有一对多关系的大表也有这个问题。

有没有更高效的方法来实现我正在寻找的东西?这似乎是一个非常简单的问题,但除了预先计算和缓存结果之外,我似乎无法找出实现这一目标的最佳方法。

这是我的查询的解释输出。注意Using temporary

id  select_type table   partitions  type    possible_keys   key key_len ref rows    filtered    Extra
1   "SIMPLE"    "a" NULL    "ref"   "PRIMARY,animals_animal_id_index"   "animals_animal_id_index"   "153"   "const" 1126303 100.00  "Using index; Using temporary"
1   "SIMPLE"    "ab"    NULL    "ref"   "animal_breeds_animal_id_breed_unique,animal_breeds_animal_id_index,animal_breeds_breed_index"  "animal_breeds_animal_id_breed_unique"  "5" "pedigreeonline.a.id"   1   100.00  "Using index"

根据要求,这里是创建表语句(我从animals 表中省略了一些不相关的列和索引)。我相信animal_breeds 表上的animal_breeds_animal_id_index 索引是多余的,因为表上有唯一键,但我们现在可以忽略它,只要它不会导致问题:)

CREATE TABLE `animals` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(150) COLLATE utf8_unicode_ci NOT NULL,
  `owner_id` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `animals_animal_id_index` (`owner_id`,`id`),
  KEY `animals_name_index` (`name`),
) ENGINE=InnoDB AUTO_INCREMENT=2470843 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci


CREATE TABLE `animal_breeds` (
  `animal_id` int(10) unsigned DEFAULT NULL,
  `breed` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  UNIQUE KEY `animal_breeds_animal_id_breed_unique` (`animal_id`,`breed`),
  KEY `animal_breeds_animal_id_index` (`animal_id`),
  KEY `animal_breeds_breed_index` (`breed`),
  CONSTRAINT `animal_breeds_animal_id_foreign` FOREIGN KEY (`animal_id`) REFERENCES `animals` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

任何帮助将不胜感激。谢谢!

【问题讨论】:

CREATE TABLE 语句会很好 使用 CREATE TABLE 语句更新 删除密钥animal_breeds_animal_id_index (animal_id), 是的,我意识到这是多余的。自从编辑我的答案后,我已经放弃了密钥,但这对查询速度没有帮助。 【参考方案1】:

了解您的数据后,您可以尝试以下方法:

SELECT
    b.*
FROM
    (
        SELECT
            DISTINCT `breed`
        FROM
            `animal_breeds`
    ) AS b
WHERE
    EXISTS (
        SELECT
            *
        FROM
            animal_breeds AS ab
            INNER JOIN animals AS a ON ab.animal_id = a.id
        WHERE
            b.breed = ab.breed
            AND a.owner_id = ?
    )
;

这个想法是在没有任何过滤的情况下获得不同品种的简短列表(对于小列表,它会非常快),然后使用相关子查询进一步过滤列表。由于列表很短,因此只会执行很少的子查询,并且它们只会检查比任何分组(distinct == grouping)快得多的存在。

这仅在您的不同列表很短时才有效。

根据您的回答随机生成数据,上述查询给了我以下执行计划:

id  select_type table   partitions  type    possible_keys   key key_len ref rows    filtered    Extra
1   PRIMARY <derived2>      ALL                 2   100.00  
3   SUBQUERY    a       ref PRIMARY,animals_animal_id_index animals_animal_id_index 153 const   1011    100.00  Using index
3   SUBQUERY    ab      ref animal_breeds_animal_id_breed_unique,`animal_breeds_animal_id_index`,animal_breeds_animal_id_index  `animal_breeds_animal_id_index` 5   test.a.id   2   100.00  Using index
2   DERIVED animal_breeds       range   animal_breeds_animal_id_breed_unique,`animal_breeds_breed_index`,animal_breeds_breed_index  `animal_breeds_breed_index` 1022        2   100.00  Using index for group-by

或者,您可以尝试像这样创建 WHERE 子句:

...
WHERE
    b.breed IN (
        SELECT
            ab.breed
        FROM
            animal_breeds AS ab
            INNER JOIN animals AS a ON ab.animal_id = a.id
        WHERE
            a.owner_id = ?
    )

【讨论】:

谢谢!接下来的几天我不在电脑旁,但我认为这可能是一个完美的解决方案。我会尽快检查并标记为已解决。【参考方案2】:

对于这个查询:

SELECT DISTINCT ab.`breed`
FROM `animal_breeds` ab INNER JOIN
     `animals` a
      ON a.`id` = ab.`animal_id` 
WHERE a.`owner_id` = ? ;

您希望在 animals(owner_id, id)animal_breeds(animal_id, breed) 上建立索引。复合索引中列的顺序很重要。

有了正确的索引,我想这会很快。

编辑:

根据说明,您使用的值有 1,126,303 个匹配项。时间是由于删除重复。考虑到表格的大小,令人惊讶的是会有这么多匹配一个值。

【讨论】:

感谢您的信息。不幸的是,我确实有这两个组合键的确切顺序。我用EXPLAIN 的输出更新了我的问题。它没有被命名为最好的,但“animals_animal_id_index”实际上是 (animal,id)。 您确定您的索引吗?我没有看到解释中“a”中提到的所有者的任何索引。如果您提供 show create table 输出,那就太好了。 Gordon 提到了索引 (owner_id, id),而不是 (animal_id, id),这是巨大的差异。 @fifonik 很抱歉造成混乱,但我确定。我在问题/cmets 中更改了一个列名(从“animal”到“owner_id”),以使其在我的 OP 中稍微不那么混乱,但显然它只是增加了更多的混乱 关于您的编辑:是的,animal_breeds 表中有很多重复的品种值。大约有 250 万条记录,但只有约 250 个独特品种。如果我在 animals 表上运行没有连接的查询,则查询只需要大约 300 毫秒,所以我认为时间不是删除重复项吗?或者在使用联接与非联接时重复删除技术是否会发生变化?我也不太清楚您所说的“令人惊讶的是会有这么多匹配一个值”是什么意思。你能解释一下吗? @Jeff 。 . .这与表中有多少品种无关。您的计划是建议一个所有者匹配animal_breeds 中的超过一百万行。考虑到桌子的大小,这似乎很多。

以上是关于从两个大表的连接中选择不同的值的主要内容,如果未能解决你的问题,请参考以下文章

添加两个变量,其中包含来自同一数据库的两个不同表的值

PostgreSQL大表的更新时间

如何删除两个大表的析取行?

从大表中有效地选择不同的(a,b)

连接来自两个不同表的两列

查询执行时间是不是因大表的不同查询参数值而异?