【问题标题】:SQL Parent/Child recursive call or union?SQL父/子递归调用或联合?
【发布时间】:2010-10-03 11:33:50
【问题描述】:

我似乎找不到相关的例子。

我正在尝试返回一个表的子集,对于该表中的每一行,我想检查它有多少个孩子,并将该数字作为结果集的一部分返回。

父表列: PK_ID、Column1、Column2、FK1

对于结果集中的每个 FK1,从 child_table 中选择 count(*)。

最终结果集

3,col1text,col2text,1(子)
5、col1texta、col2texta、2(子)
6、col1textb、col2textb、0(子)
9, col1textc, col2textc, 4(子)

我正在努力寻找在另一个查询中引用结果集中的列的最佳方法,然后再次将它们连接在一起。使用 T-sql

【问题讨论】:

  • 我很难弄清楚您要达到什么目的才能找到答案。如果 FK1 是保存子键的外键,那么对于任何具有 FK = 1 的行,FK1 = 1 的数量都相同。为了清楚起见,你能举一个 sql 示例吗?
  • FK1 中的值是孩子的 PK,因此它将只有一行具有该值。
  • 添加了 SQL Server 和 Oracle 基准测试。

标签: sql sql-server tsql sybase


【解决方案1】:

好的,显然,基于对另一个答案的支持,这需要进一步解释。示例(使用 MySQL 完成,因为我很方便,但原理对任何 SQL 方言都是通用的):

CREATE TABLE Blah (
  ID INT PRIMARY KEY,
  SomeText VARCHAR(30),
  ParentID INT
)

INSERT INTO Blah VALUES (1, 'One', 0);
INSERT INTO Blah VALUES (2, 'Two', 0);
INSERT INTO Blah VALUES (3, 'Three', 1);
INSERT INTO Blah VALUES (4, 'Four', 1);
INSERT INTO Blah VALUES (5, 'Five', 4);

左加入版本:

SELECT a.ID, a.SomeText, COUNT(1)
FROM Blah a
JOIN Blah b ON a.ID= b.ParentID
GROUP BY a.ID, a.SomeText

错了。忽略没有孩子的情况。

左外连接:

SELECT a.ID, a.SomeText, COUNT(1)
FROM Blah a
LEFT OUTER JOIN Blah b ON a.ID= b.ParentID
GROUP BY a.ID, a.SomeText

错了,原因有点微妙。 COUNT(1) 计数 NULL 行,而 COUNT(b.ID) 不计数。所以上面是错误的,但这是正确的:

SELECT a.ID, a.SomeText, COUNT(b.ID)
FROM Blah a
LEFT OUTER JOIN Blah b ON a.ID= b.ParentID
GROUP BY a.ID, a.SomeText

相关子查询:

SELECT ID, SomeText, (SELECT COUNT(1) FROM Blah WHERE ParentID= a.ID) ChildCount
FROM Blah a

也正确。

好的,那么使用哪个?计划只能告诉你这么多。 subqueries vs left-joins 的问题是一个老问题,没有基准测试就没有明确的答案。所以我们需要一些数据:

<?php
ini_set('max_execution_time', 180);

$start = microtime(true);

echo "<pre>\n";

mysql_connect('localhost', 'scratch', 'scratch');
if (mysql_error()) {
    echo mysql_error();
    exit();
}
mysql_select_db('scratch');
if (mysql_error()) {
    echo mysql_error();
    exit();
}

$count = 0;
$limit = 1000000;
$this_level = array(0);
$next_level = array();

while ($count < $limit) {
    foreach ($this_level as $parent) {
        $child_count = rand(0, 3);
        for ($i=0; $i<$child_count; $i++) {
            $count++;
            query("INSERT INTO Blah (ID, SomeText, ParentID) VALUES ($count, 'Text $count', $parent)");
            $next_level[] = $count;
        }
    }
    $this_level = $next_level;
    $next_level = array();
}

$stop = microtime(true);
$duration = $stop - $start;
$inserttime = $duration / $count;

echo "$count users added.\n";
echo "Program ran for $duration seconds.\n";
echo "Insert time $inserttime seconds.\n";
echo "</pre>\n";

function query($query) {
    mysql_query($query);
    if (mysql_error()) {
        echo mysql_error();
        exit();
    }
}
?>

我在这次运行期间内存不足 (32M),所以最终只记录了 876,109 条记录,但嘿,它会的。后来,当我测试 Oracle 和 SQL Server 时,我将完全相同的数据集导入 Oracle XE 和 SQL Server Express 2005。

现在另一位发帖人提出了我在查询周围使用计数包装器的问题。他正确地指出,在这种情况下优化器可能不会执行子查询。 MySQL 似乎没有那么聪明。甲骨文是。 SQL Server 似乎也是如此。

所以我将为每个数据库查询组合引用两个数字:第一个包含在 SELECT COUNT(1) FROM ( ... ) 中,第二个是原始的。

设置:

  • MySQL 5.0 使用 PremiumSoft Navicat(LIMIT 10000 查询);
  • 使用 Microsoft SQL Server Management Studio Express 的 SQL Server Express 2005;
  • 使用 PL/SQL Developer 7 的 Oracle XE(限制为 10,000 行)。

左外连接:

SELECT a.ID, a.SomeText, COUNT(b.ID)
FROM Blah a
LEFT OUTER JOIN Blah b ON a.ID= b.ParentID
GROUP BY a.ID, a.SomeText
  • MySQL: 5.0:51.469s / 49.907s
  • SQL Server: 0(1) / 9s(2)
  • Oracle XE: 1.297s / 2.656s

(1) 几乎是瞬时的(确认不同的执行路径)
(2) 令人印象深刻的是它返回所有行,而不是 10,000

只是去展示一个真实数据库的价值。此外,删除 SomeText 字段对 MySQL 的性能有重大影响。此外,10000 的限制与 MySQL 没有它之间没有太大区别(性能提高了 4-5 倍)。 Oracle 拥有它只是因为 PL/SQL Developer 在内存使用量达到 100M 时大吃一惊。

相关子查询:

SELECT ID, SomeText, (SELECT COUNT(1) FROM Blah WHERE ParentID= a.ID) ChildCount
FROM Blah a
  • MySQL: 8.844s / 11.10s
  • SQL Server: 0s / 6s
  • 甲骨文: 0.046s / 1.563s

因此 MySQL 要好 4-5 倍,Oracle 的速度大约是前者的两倍,而 SQL Server 可以说只是快一点。

重点是:相关子查询版本在所有情况下都更快。

相关子查询的另一个优点是它们在语法上更清晰且更易于扩展。我的意思是,如果您想在一堆其他表中进行计数,每个表都可以干净轻松地作为另一个选择项包含在内。例如:想象一下客户对发票的记录,其中这些发票要么未付,要么逾期,要么已付。使用简单的子查询:

SELECT id,
  (SELECT COUNT(1) FROM invoices WHERE customer_id = c.id AND status = 'UNPAID') unpaid_invoices,
  (SELECT COUNT(1) FROM invoices WHERE customer_id = c.id AND status = 'OVERDUE') overdue_invoices,
  (SELECT COUNT(1) FROM invoices WHERE customer_id = c.id AND status = 'PAID') paid_invoices
FROM customers c

聚合版丑多了。

现在我并不是说子查询总是优于聚合连接,但通常情况下,您必须对其进行测试。根据您的数据、数据的大小和您的 RDBMS 供应商,差异可能非常显着。

【讨论】:

  • count(b.id) 将通过左外连接给出正确的结果。它还将强制进行一次顺序扫描和一次索引扫描。相关子查询会强制嵌套循环,这意味着性能要差几个数量级。
  • @cletus:使用 COUNT(b.parentID) 而不是 COUNT(1)。由于 LEFT OUTER JOIN,b 为 NULL,它不会影响计数。此外,左连接始终是外连接。
  • 请参阅我的回答,了解为什么您的测量结果根本不正确。
  • Cletus,这些数字令人印象深刻,但仍然没有意义。你能展示一下解释计划吗?我还是不明白 N^2 怎么会小于 2N。
【解决方案2】:

相信这就是你想要做的:

SELECT P.PK_ID, P.Column1, P.Column2, COUNT(C.PK_ID)
FROM
    Parent P
    LEFT JOIN Child C ON C.PK_ID = P.FK1
GROUP BY
    P.PK_ID, P.Column1, P.Column2

【讨论】:

  • 看起来它对我来说处理零个孩子。 +1
  • 试试看。如果您在没有孩子的情况下进行左连接,则不会出现行,并且不会显示“0 个孩子”。它不会出现。
  • 而且左外连接也不会这样做,因为除非您尝试过滤,否则空行的计数将为 1。正确的解决方案是我发布的答案。
  • 在 T-SQL 中,“LEFT JOIN”意味着“LEFT OUTER JOIN”。此外,虽然 COUNT(*) 计算空值,但 COUNT(Column) 不计算。所以结果是预期的(当没有匹配的子行时为 0)。
  • 查看我的答案以获得进一步的解释。
【解决方案3】:

解释@cletus 错误的原因。

首先,做研究的道具。

第二,你做错了。

解释:

原始查询:

EXPLAIN
SELECT ID, (SELECT COUNT(1) FROM Blah WHERE ParentID= a.ID) as ChildCount
FROM Blah a

结果:

“在 blah a 上进行 Seq 扫描(成本=0.00..145180063607.45 行=2773807 宽度=4)” “子计划” “ -> 聚合(成本=52339.61..52339.63 行=1 宽度=0)” “ -> 对 blah 的 Seq 扫描(成本=0.00..52339.59 行=10 宽度=0)” " 过滤器: (parentid = $0)"

当你用“select count(1)”换行时会发生什么:

EXPLAIN SELECT count(1) FROM (
SELECT ID, (SELECT COUNT(1) FROM Blah WHERE ParentID= a.ID) as ChildCount
FROM Blah a) as bar
“聚合(成本=52339.59..52339.60 行=1 宽度=0)” “ -> 在 blah a 上进行 Seq 扫描(成本=0.00..45405.07 行=2773807 宽度=0)”

注意到区别了吗?

优化器足够聪明,可以看到它不需要执行子查询。所以并不是相关的子查询很快;就是不这样做很快:-)。

不幸的是,它不能对左外连接做同样的事情,因为结果的数量不是由第一次扫描预先确定的。

第 1 课:查询计划告诉你很多。糟糕的实验设计会给你带来麻烦。

第 1.1 课:如果您不需要加入,请务必不要。

我创建了一个包含大约 270 万个查询的测试数据集。

左外连接(没有包装器)在我的笔记本电脑上运行了 171,757 毫秒。

相关子查询...我会在它完成后更新,我在 700K ms 并且它仍在运行。

第 2 课:如果有人告诉您查看查询计划,并声称它显示了算法顺序的差异……请查看查询计划。

【讨论】:

  • 我在关联子查询运行 50 分钟后终止了它,但没有产生答案。请记住,左外部联接在不到 3 分钟的时间内运行。
  • -1 实际上我检查了两个版本(带和不带包装的 COUNT(1))并且持续时间大致相同。衡量实际结果和理论上的解释计划是有区别的。
  • 您能否发布运行时间并解释两个查询的计划? MySQL 可能做了一些我在上面运行的 Postgres 没有做的聪明的事情。鉴于两者的性质,可能性极小,但有可能..
  • 在决定是否需要执行子查询时,MySQL 可能不如 SQL Server 聪明。我确实检查过。我一直在尝试让我的 SQL Server Express 正常工作并导入大量数据,这是一场噩梦,但如果/当它正常工作时,我也会发布它。
【解决方案4】:

您是否曾尝试为 MySQL 的父 ID 添加索引。我很确定执行时间会大大改善。尚未测试,但我会说 MySQL 会遍历所有行来确定计数。这意味着它在这 59 秒内进行了 10 - 400 亿次(表中的行数 * 10000)次查找。

假设 SQL Server 和 Oracle 即时创建索引。如果他们这样做,那将只有 1 到 400 万。

【讨论】:

    【解决方案5】:

    您的查询都假定输入父子节点的顺序是连续的。如果最后输入来自第一个节点之一的子节点,并且其 ID 或 PK 较高,则查询不起作用。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-05-19
      • 1970-01-01
      • 1970-01-01
      • 2014-06-02
      • 2013-12-24
      • 1970-01-01
      • 2016-05-17
      • 1970-01-01
      相关资源
      最近更新 更多