好的,显然,基于对另一个答案的支持,这需要进一步解释。示例(使用 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
也正确。
好的,那么使用哪个?计划只能告诉你这么多。子查询与左连接的问题是一个古老的问题,如果不进行基准测试就没有明确的答案。所以我们需要一些数据:
<?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
查询中);
- SQL Server Express 2005 使用 Microsoft SQL Server Management Studio Express;
- 使用 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 供应商,差异可能非常显着。