【问题标题】:In MySQL is it faster to execute one JOIN + one LIKE statement or two JOINs?在 MySQL 中执行一个 JOIN + 一个 LIKE 语句还是两个 JOIN 更快?
【发布时间】:2017-04-24 14:15:10
【问题描述】:

我必须创建一个 cron 作业,这本身很简单,但因为它会每分钟运行一次,所以我担心性能。我有两张表,一张有用户名,另一张有关于他们网络的详细信息。大多数时候,一个用户只属于一个网络,但理论上他们可能属于更多网络,但即使这样也很少,可能是两个或三个。因此,为了减少 JOIN 的数量,我将由| 分隔的网络 ID 保存在用户表的一个字段中,例如

|1|3|9|

(为这个问题简化的)用户表结构是

TABLE `users` (
  `u_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE,
  `userid` VARCHAR(500) NOT NULL UNIQUE,
  `net_ids` VARCHAR(500) NOT NULL DEFAULT '',
  PRIMARY KEY (`u_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

(也简化的)网络表结构是

CREATE TABLE `network` (
  `n_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE,
  `netname` VARCHAR(500) NOT NULL UNIQUE,
  `login_time` DATETIME DEFAULT NULL,
  `timeout_mins` TINYINT UNSIGNED NOT NULL DEFAULT 10,
  PRIMARY KEY (`n_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

发生超时时我必须发送警告,我的查询是

SELECT N.netname, N.timeout_mins, N.n_id, U.userid FROM
(SELECT netname, timeout_mins, n_id FROM network
 WHERE is_open = 1 AND notify = 1
 AND TIMESTAMPDIFF(SECOND, TIMESTAMPADD(MINUTE, timeout_mins, login_time), NOW()) < 60) AS N
INNER JOIN users AS U ON U.net_ids LIKE CONCAT('%|', N.n_id, '|%');

我将 N 设为子查询以减少加入的行数。但是我想知道添加以 u_id 和 n_id 作为列的第三个表是否会更快,从用户中删除 net_ids 列,然后对所有三个表进行连接?因为我读到使用 LIKE 会减慢速度。

在这种情况下,最有效的查询是什么?一个 JOIN 和一个 LIKE 还是两个 JOIN?

附:我做了一些实验,使用两个 JOIN 的初始值高于使用一个 JOIN 和一个 LIKE。但是,重复运行相同的查询似乎会加快速度,我怀疑某些东西缓存在某处,无论是在我的应用程序还是数据库中,并且两者都具有可比性,所以我觉得这个数据并不令人满意。根据我所阅读的内容,这也与我的预期相矛盾。

我用过这张桌子:

TABLE `user_net` (
`u_id` BIGINT UNSIGNED NOT NULL,
`n_id` BIGINT UNSIGNED NOT NULL,
INDEX `u_id` (`u_id`),
FOREIGN KEY (`u_id`) REFERENCES `users`(`u_id`),
INDEX `n_id` (`n_id`),
FOREIGN KEY (`n_id`) REFERENCES `network`(`n_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

这个查询:

SELECT N.netname, N.timeout_mins, N.n_id, U.userid FROM
(SELECT netname, timeout_mins, n_id FROM network
 WHERE is_open = 1 AND notify = 1
 AND TIMESTAMPDIFF(SECOND, TIMESTAMPADD(MINUTE, timeout_mins, login_time), NOW()) < 60) AS N
INNER JOIN user_net AS UN ON N.n_id = UN.n_id
INNER JOIN users AS U ON UN.u_id = U.u_id;

【问题讨论】:

  • :-( 不要那样做。
  • 检查执行计划。不要猜测。虽然,LIKE '%something%' 总是很慢 - 没有办法使用带有这样一个字符串的索引,这意味着将扫描 每一行
  • 不要使用我目前的方法?我意识到这不是理想的设计,但我主要关心的是减少此查询的时间。
  • 顺便说一句,如果您 net_ids 字段包含一个带有分隔值的字符串,那么您有一个严重的(实际上是癌症中的终端)设计错误 - 您通过在单个单元格中存储多个值来破坏 1NF .使用单独的表格
  • 如何查看执行计划?我无权访问 phpMyAdmin。我的客户没有给我 cPanel 访问权限。我一直在使用自己制作的简单表单来运行我必须执行的查询,例如创建表。

标签: mysql performance join sql-like


【解决方案1】:

您应该为user_net 表定义composite indexes。其中一个可以(并且应该)是主键。

TABLE `user_net` (
    `u_id` BIGINT UNSIGNED NOT NULL,
    `n_id` BIGINT UNSIGNED NOT NULL,
    PRIMARY KEY (`u_id`, `n_id`),
    INDEX `uid_nid` (`n_id`, `u_id`),
    FOREIGN KEY (`u_id`) REFERENCES `users`(`u_id`),
    FOREIGN KEY (`n_id`) REFERENCES `network`(`n_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

我还将您的查询重写为:

SELECT N.netname, N.timeout_mins, N.n_id, U.userid
FROM network N
INNER JOIN user_net AS UN ON N.n_id = UN.n_id
INNER JOIN users AS U  ON UN.u_id   = U.u_id
WHERE N.is_open = 1 
  AND N.notify = 1
  AND TIMESTAMPDIFF(SECOND, TIMESTAMPADD(MINUTE, N.timeout_mins, N.login_time), NOW()) < 60

虽然您的子查询可能不会造成太大影响,但也没有必要。

请注意,最后一个条件不能使用索引,因为您必须合并两列。如果您的 MySQL 版本至少为 5.7.6,您可以定义 indexed virtual (calculated) column

CREATE TABLE `network` (
  `n_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE,
  `netname` VARCHAR(500) NOT NULL UNIQUE,
  `login_time` DATETIME DEFAULT NULL,
  `timeout_mins` TINYINT UNSIGNED NOT NULL DEFAULT 10,
  `is_open` TINYINT UNSIGNED,
  `notify`  TINYINT UNSIGNED,
  `timeout_dt` DATETIME AS (`login_time` + INTERVAL `timeout_mins` MINUTE),
  PRIMARY KEY (`n_id`),
  INDEX (`timeout_dt`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

现在将查询更改为:

SELECT N.netname, N.timeout_mins, N.n_id, U.userid
FROM network N
INNER JOIN user_net AS UN ON N.n_id = UN.n_id
INNER JOIN users AS U  ON UN.u_id   = U.u_id
WHERE N.is_open = 1 
  AND N.notify  = 1
  AND N.timeout_dt < NOW() + INTERVAL 60 SECOND

它将能够使用索引。

你也可以尝试替换

INDEX (`timeout_dt`)

INDEX (`is_open`, `notify`, `timeout_dt`)

看看有没有用。

【讨论】:

  • 谢谢!我正在使用版本 4.0something(我无法控制)。我会尝试其余的然后回来。
  • 如果主键和索引都相同,是否需要使用它们?我以为主键会自动编入索引。
  • @inarilo 索引中列的顺序很重要。 index(a, b)index(b, a) 不同。对于多对多表,您通常需要两者。顺便说一句:您应该早点告诉我们,您使用的是旧版本(至少 10 年)——这可能解释了 JOIN 性能不佳的问题。
  • 哦,我明白了。关于版本,我没有意识到它会影响 JOIN 性能。但我认为与使用 LIKE 的 JOIN 相比,它仍然应该相对更快。
  • 我的运行时仍然不一致 :( 但你的回答绝对有帮助,所以我赞成它。我可能会选择第三种选择,不理想,但比我的不理想原始解决方案。
【解决方案2】:

重新制定以避免在函数中隐藏列。我无法理解您的日期表达,但请注意:

login_time < NOW() - INTERVAL timeout_mins MINUTE

如果你能实现这样的目标,那么这个索引应该会有所帮助:

INDEX(is_open, notify, login_time)

如果这还不够好,让我们看看其他公式,以便我们可以比较它们。

用逗号(或|)分隔内容可能是一个非常糟糕的主意。

底线:假设JOINs 不是性能问题,根据需要编写带有尽可能多的JOINs 的查询。 那么让我们优化那个

【讨论】:

  • 有趣,我只是在看到这个之前尝试了几分钟:) 但每次我尝试定义一个包含三列的索引时,它只接受前两列。我想对其进行排序,所以我使用ALTER TABLE network ADD INDEX notify_list (notify DESC, is_open DESC, login_time ASC);,但即使我删除了排序顺序,它仍然只接受两个列,我实际上回到 SE 来寻找解决方案。 :/ 这是版本 4 的问题吗?
  • 对不起,没关系,我用 describe 来查看表定义,它只是为第一列指示 MUL,索引很好。
  • @inarilo - ASCDESCINDEX 声明中被接受,但被忽略。因此,(直到 8.0)优化器不会使用混合方向优化 ORDER BY。我瞎了 - 如果你想更多讨论,请告诉我们CREATE TABLE(不是DESCRIBE)和EXPLAIN
  • 您好,感谢您的帮助,但我最终决定取消连接并使用包含所有必要值的单独表。在添加网络时为每个网络插入子记录,以及相关的用户名。通知时间随登录和注销而更新。我认为这比重复运行相同的连接查询更有意义。我的 cron 作业只会从这个表中选择我在通知时间索引的记录。
猜你喜欢
  • 2011-06-13
  • 2016-09-13
  • 1970-01-01
  • 1970-01-01
  • 2018-07-22
  • 2010-12-21
  • 2021-07-13
  • 2012-05-19
相关资源
最近更新 更多