Хорошо, по-видимому, основываясь на ответах на другой ответ, это требует дополнительного объяснения. Пример (сделано с 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
Также правильно.
Хорошо, так что же использовать? Планы только так много тебе скажут. Вопрос о подзапросах против левых соединений является старым, и нет четкого ответа без сравнительного анализа. Итак, нам нужны некоторые данные:
<code><?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 "
\ п ";
запрос функции ($ query) {
mysql_query ($ запроса);
if (mysql_error ()) {
echo mysql_error ();
выход();
}
}
?>
Мне не хватило памяти (32M) во время этого прогона, так что в итоге получилось только 876 109 записей, но это будет хорошо. Позже, когда я тестирую Oracle и SQL Server, я беру один и тот же набор данных и импортирую его в Oracle XE и SQL Server Express 2005.
Теперь другой постер поднял вопрос о том, чтобы я использовал оболочку count для запросов. Он правильно указал, что оптимизатор может не выполнять подзапросы в этом случае. MySQL не кажется таким уж умным. Оракул есть. SQL Server, похоже, тоже.
Итак, я процитирую две цифры для каждой комбинации базы данных и запроса: первая обернута в SELECT COUNT(1) FROM ( ... )
, вторая - в необработанную.
Установка:
- MySQL 5.0 с использованием PremiumSoft Navicat (
LIMIT 10000
в запросе);
- SQL Server Express 2005 с использованием Microsoft SQL Server Management Studio Express;
- Oracle XE с использованием PL / SQL Developer 7 (ограничено 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.469 с / 49.907 с
- SQL Server: 0 (1) / 9s (2)
- Oracle XE: 1,297 с / 2,665 с
(1) Практически мгновенно (подтверждая другой путь выполнения)
(2) Впечатляет, учитывая, что он возвращает все строки, а не 10000
Просто показывает ценность реальной базы данных. Кроме того, удаление поля SomeText оказало значительное влияние на производительность MySQL. Также не было большой разницы между лимитом 10000 и отсутствием его с MySQL (повышение производительности в 4-5 раз). У Oracle это было просто потому, что разработчик PL / SQL сорвался, когда он достиг 100M.
Коррелированный подзапрос:
SELECT ID, SomeText, (SELECT COUNT(1) FROM Blah WHERE ParentID= a.ID) ChildCount
FROM Blah a
- MySQL: 8,844 с / 11,10 с
- SQL Server: 0 с / 6 с
- Oracle: 0,046 с / 1,563 с
Таким образом, 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 разница может быть очень значительной.