【问题标题】:SQL JOIN vs. multiple SELECT statementsSQL JOIN 与多个 SELECT 语句
【发布时间】:2013-06-11 13:54:44
【问题描述】:

每个使用数据库的开发人员都有这个问题。 而且您通常无法估计一个表在 x 年内将有多少条记录。

尤其是在使用 O/R 映射器时,这非常不舒服!

为什么 RDBMS 驱动程序不能解决这个问题? 为什么多次传输记录而不是一次然后引用它。 对于客户端应用程序,这可能是完全透明的。甚至提供高级功能。 尤其是使用 OR 映射器,创建类似于 DB 数据的子 bean 甚至可能非常有用,仅作为参考。

如果您可以在不知道冗余数据的情况下加入 1:n 表,那就太好了。

有人知道这样优化的 RDBMS 吗? 还是不能这样做?如果是这样,为什么?

----- ---- 编辑 ----- -----
@Thilo:感谢您的链接。很有趣。

我已经使用 XAMPP for Windows 进行了测试。
PHP:5.4.7
MySQL:5.5.27
结果表明,在 MySQL 中使用 JOIN 一定要小心。

每次执行 JOIN 时都会得到重复的数据(1:1 除外)。为什么要多次传输这些数据?

测试:

我创建了两个表。表 A 有 500 条记录和 9 列 VARCHAR(32) 和表 B 有 50000 条记录。 (1:100)

SET @numA = 500;
SET @numBperA = 100;

DROP TABLE IF EXISTS `table_b`;
DROP TABLE IF EXISTS `table_a`;

DROP PROCEDURE IF EXISTS fill_table_b;
DROP PROCEDURE IF EXISTS fill_table_a;


CREATE TABLE `table_a` (
  `id`   int(11)     NOT NULL,
  `val1` varchar(32) NOT NULL,
  `val2` varchar(32) NOT NULL,
  `val3` varchar(32) NOT NULL,
  `val4` varchar(32) NOT NULL,
  `val5` varchar(32) NOT NULL,
  `val6` varchar(32) NOT NULL,
  `val7` varchar(32) NOT NULL,
  `val8` varchar(32) NOT NULL,
  `val9` varchar(32) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1;

delimiter $$
CREATE PROCEDURE fill_table_a()
BEGIN
    DECLARE i INT DEFAULT 1;
    SET i = 1;
    WHILE ( i <= @numA) DO
        INSERT INTO table_a (id, val1, val2, val3, val4, val5, val6, val7, val8, val9)
        VALUES (i, md5(rand()), md5(rand()), md5(rand()), md5(rand()), md5(rand()), md5(rand()), md5(rand()), md5(rand()), md5(rand()));
        SET i=i+1;
    END WHILE;
END$$
delimiter ;

call fill_table_a();


CREATE TABLE IF NOT EXISTS `table_b` (
  `id`         int(11)     NOT NULL AUTO_INCREMENT,
  `table_a_id` int(11)     NOT NULL,
  `val`        varchar(32) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `table_a_id` (`table_a_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

ALTER TABLE `table_b` ADD CONSTRAINT `table_b_ibfk_1` FOREIGN KEY (`table_a_id`) REFERENCES `table_a` (`id`);


delimiter $$
CREATE PROCEDURE fill_table_b()
BEGIN
    DECLARE i INT DEFAULT 1;
    DECLARE j INT DEFAULT 1;
    SET i = 1;
    WHILE (i <= @numA) DO
        SET j = 1;
        WHILE (j <= @numBperA) DO
            INSERT INTO table_b (table_a_id, val)
            VALUES (i, md5(rand()));
            SET j=j+1;
        END WHILE;
        SET i=i+1;
    END WHILE;
END$$
delimiter ;

call fill_table_b();

现在我想从表 A 中选择 300 行,从表 B 中选择相关的 30000 行。

我做了这 3 种方法:

使用单个请求选择 A JOIN B

$time = microtime(true);
for( $i = 0; $i < 50; $i++ ) {
  $resultA = mysqli_query($link, "SELECT * FROM table_a LEFT JOIN table_b ON table_b.table_a_id = table_a.id WHERE table_a.id BETWEEN 100 AND 399");
  $resultArray = array();
  //while( $resultArray[] = mysqli_fetch_assoc($resultA) ) {}
  $numRows = mysqli_num_rows($resultA);
}
$time2 = microtime(true);
echo("numSelectedRows: " . $numRows . "<br>time: " . number_format($time2 - $time, 3) . " sec.<br>Memory: " . number_format(memory_get_peak_usage() / 1024 / 1024, 3) . " MiB");
  • 使用抓取
    numSelectedRows:30000
    时间:15.539 秒。
    内存:55.649 MiB

  • 没有获取
    numSelectedRows:30000
    时间:6.262 秒。
    内存:3.431 MiB

通过单个请求选择 A。遍历 Result 并向表 B 发出 300 个请求。

$time = microtime(true);
for( $i = 0; $i < 50; $i++ ) {
  $numRowsB = 0;
  $resultA = mysqli_query($link, "SELECT * FROM table_a WHERE table_a.id BETWEEN 100 AND 399");
  while( $row = mysqli_fetch_assoc($resultA) ) {
    $resultB = mysqli_query($link, "SELECT * FROM table_b WHERE table_b.table_a_id = " . $row['id']);
    while( mysqli_fetch_assoc($resultB) ) {}
    $numRowsB += mysqli_num_rows($resultB);
  }
}
$numRowsA = mysqli_num_rows($resultA);
$time2 = microtime(true);
echo("numSelectedRows A: " . $numRowsA . "<br>numSelectedRows B: " . $numRowsB . "<br>time: " . number_format($time2 - $time, 3) . " sec.<br>Memory: " . number_format(memory_get_peak_usage() / 1024 / 1024, 3) . " MiB");
  • 带提取
    numSelectedRows A:300
    numSelectedRows B:30000
    时间:7.713 秒。
    内存:0.364 MiB

通过单个请求选择 A。通过单个请求选择 B。

$time = microtime(true);
for( $i = 0; $i < 50; $i++ ) {
  $resultA = mysqli_query($link, "SELECT * FROM table_a WHERE table_a.id BETWEEN 100 AND 399");
  $resultB = mysqli_query($link, "SELECT * FROM table_b WHERE table_b.table_a_id BETWEEN 100 AND 399");
  $resultArray = array();
  //while( $resultArray[] = mysqli_fetch_assoc($resultA) ) {}
  //while( $resultArray[] = mysqli_fetch_assoc($resultB) ) {}
}
$numRowsA = mysqli_num_rows($resultA);
$numRowsB = mysqli_num_rows($resultB);
$time2 = microtime(true);
echo("numSelectedRows A: " . $numRowsA . "<br>numSelectedRows B: " . $numRowsB . "<br>time: " . number_format($time2 - $time, 3) . " sec.<br>Memory: " . number_format(memory_get_peak_usage() / 1024 / 1024, 3) . " MiB");
  • 使用抓取
    numSelectedRows A:300
    numSelectedRows B:30000
    时间:6.020 秒。
    内存:15.928 MiB

  • 没有获取
    numSelectedRows A:300
    numSelectedRows B:30000
    时间:3.018 秒。
    内存:1.156 MiB

【问题讨论】:

标签: sql sql-server performance rdbms


【解决方案1】:

为什么 RDBMS 驱动程序不能解决这个问题?

出于同样的原因,查询优化器有时也无法正确处理。

这很难。

数据库(和其他软件)尽最大努力优化查询执行,但有时您仍然需要手动“帮助它”。

如果有的话,我宁愿只有数据库本身甚至在这里尝试它的手,而不是让其他层(例如 OR/映射器或数据库驱动程序)也“自动”摆弄。否则,该过程将变得完全不可预测,并且在必要时难以控制。

【讨论】:

  • 这与查询执行优化无关。它是关于通过套接字传输冗余数据的。即使不了解底层数据结构,冗余数据也应该易于压缩。那为什么 JOIN 会更慢呢?
  • 如果网络协议已经使用某种形式的压缩,我不会感到惊讶。但是,与数据库查询中发生的其他事情相比,这(通过网络发送重复数据的开销)真的是一个问题吗?
  • “如果网络协议已经使用某种形式的压缩,我不会感到惊讶”:jonathanlewis.wordpress.com/2010/05/07/sqlnet-compression“SQLNet 已经能够为年,至少从 8.0 开始。如果单个会话数据单元 (SDU) 包含具有适当重复模式的数据,SQLNet 可以在传输之前删除重复项(幸运的是,将它们放回接收端)。”
【解决方案2】:

因为 RDBMS 驱动程序不返回结构化实体而是通用数据集,并且无法知道返回的数据字段是如何相互关联的:是一些相关行的计数要保留在应用程序中,只是因为它有点命名在查询中?

如果这么简单,早就有人发现了宇宙的奥秘,我很乐意签署表格并退休!! ;))

遗憾的是,您需要执行的查询取决于架构、数据重新分区以及最后但并非最不重要的业务规则和应用程序功能。因此,在某些情况下,您将需要有或没有分组的联接,否则多个查询会更好,就这样吧。

【讨论】:

  • 是的,驱动程序向应用程序提供非结构化数据。但是谁说,从驱动程序到数据库的连接必须全部是未结构化的数据?我对 DB-internals 并不深入。我只是想知道,为什么即使是两个表的简单 JOIN 也会比第一条语句和每行的进一步语句慢。也许我错了,但我认为这是通过 JOIN 传输大量数据的结果。因为第二个表中的所有重复行。我错了吗?
  • 这取决于很多事情:表中有多少数据,查询中如何使用索引,在您的集合中过滤和返回多少数据,查询是否被缓存等. 两者都是响应不同需求的不同工具。最后,由您决定每种情况下哪个更好。当我需要检索大量数据时,我很少发现无法优化并且在我的应用程序层中最终比多个查询性能更高的连接查询。对于多个查询,您往往会在沟通、查询编译和优化方面浪费时间。
猜你喜欢
  • 2010-09-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-02-16
  • 2012-05-19
  • 1970-01-01
  • 2021-03-27
相关资源
最近更新 更多