Рекурсивный вызов или объединение SQL Parent/Child?

Кажется, я не могу найти соответствующий пример.

Я пытаюсь вернуть подмножество таблицы, и для каждой строки в этой таблице я хочу проверить, сколько у нее дочерних элементов, и вернуть это число как часть набора результатов.

Столбцы родительской таблицы: PK_ID, Column1, Column2, FK1

Для каждого FK1 в результирующем наборе выберите count(*) из child_table.

Окончательный набор результатов

3, col1text, col2text, 1(дочерний)
5, col1texta, col2texta, 2(дочерний)
6, col1textb, col2textb, 0(дочерний)
9, col1textc, col2textc, 4(дочерний)< бр>

Я изо всех сил пытаюсь найти лучший способ сослаться на столбец в наборе результатов в другом запросе, а затем снова соединить их вместе. Использование T-sql


person Community    schedule 26.01.2009    source источник
comment
Мне трудно понять, чего вы пытаетесь достичь, чтобы найти ответ. Если FK1 является внешним ключом для хранения дочерних ключей, то для любой строки, имеющей FK = 1, будет одинаковое количество FK1 = 1. Не могли бы вы привести пример sql для ясности?   -  person Brettski    schedule 26.01.2009
comment
Значение в FK1 — это PK дочернего элемента, поэтому у него будет только одна строка с этим значением.   -  person Brettski    schedule 26.01.2009
comment
Добавлены тесты SQL Server и Oracle.   -  person cletus    schedule 26.01.2009


Ответы (5)


Хорошо, по-видимому, на основании голосов за другой ответ это требует дальнейшего объяснения. Пример (сделано с 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

Тоже правильно.

Хорошо, так что использовать? Планы только говорят вам так много. Проблема подзапросов и 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();
    }
}
?>

У меня закончилась память (32 МБ) во время этого прогона, поэтому в итоге получилось только 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) / 9 с(2)
  • Oracle XE: 1,297 с / 2,656 с

(1) Практически мгновенно (подтверждение другого пути выполнения)
(2) Впечатляет, учитывая, что возвращаются все строки, а не 10 000

Просто показывает ценность реальной базы данных. Кроме того, удаление поля SomeText оказало значительное влияние на производительность MySQL. Также не было большой разницы между ограничением в 10000 и его отсутствием с MySQL (улучшение производительности в 4-5 раз). У Oracle это было только потому, что PL/SQL Developer вырвало, когда он достиг 100-мегабайтного использования памяти.

Связанный подзапрос:

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

Агрегатная версия намного уродливее.

Я не утверждаю, что подзапросы всегда лучше агрегатных объединений, но достаточно часто это так, что вам нужно проверить это. В зависимости от ваших данных, размера этих данных и вашего поставщика РСУБД разница может быть очень значительной.

person cletus    schedule 26.01.2009
comment
count(b.id) даст правильный результат с левым внешним соединением. Это также вызовет одно последовательное сканирование и одно сканирование индекса. Коррелированные подзапросы вызывают вложенный цикл, что означает снижение производительности на несколько порядков. - person SquareCog; 26.01.2009
comment
@cletus: используйте COUNT(b.parentID) вместо COUNT(1). Если b имеет значение NULL из-за LEFT OUTER JOIN, это не будет способствовать подсчету. Кроме того, LEFT JOIN всегда являются внешними соединениями. - person Bill Karwin; 26.01.2009
comment
Пожалуйста, смотрите мой ответ для объяснения того, почему ваши измерения в корне неверны. - person SquareCog; 26.01.2009
comment
Клетус, это очень впечатляющие цифры, которые до сих пор не имеют смысла. Не могли бы вы показать план объяснения? Я до сих пор не понимаю, как N^2 может быть меньше 2N. - person SquareCog; 26.01.2009

Я полагаю, что вы пытаетесь сделать:

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
person Sean Bright    schedule 26.01.2009
comment
Похоже, он обрабатывает ноль детей для меня. +1 - person Amy B; 26.01.2009
comment
Попробуй это. Если вы выполните левое соединение, когда нет дочерних элементов, строки не будет, и в ней не будет указано 0 дочерних элементов. Просто не появится. - person cletus; 26.01.2009
comment
И левое внешнее соединение тоже этого не сделает, потому что вы получите 1 для нулевой строки, если не попытаетесь отфильтровать это. Правильное решение - это ответ, который я опубликовал. - person cletus; 26.01.2009
comment
В T-SQL «ЛЕВОЕ СОЕДИНЕНИЕ» подразумевает «ЛЕВО ВНЕШНЕЕ СОЕДИНЕНИЕ». Кроме того, в то время как COUNT(*) подсчитывает нулевые значения, COUNT(Column) этого не делает. Таким образом, результат является ожидаемым (0, когда нет соответствующей дочерней строки). - person Sean Bright; 26.01.2009
comment
Смотрите мой ответ для дальнейшего объяснения. - person cletus; 26.01.2009
comment
Проверено (ниже). Этот запрос как минимум в 5 раз медленнее коррелированного подзапроса. - person cletus; 26.01.2009
comment
Есть сейчас. Спасибо за исследовательский комментарий, хотя и с -2 чистыми голосами, я действительно не уверен, почему я беспокоюсь. - person cletus; 26.01.2009

Объяснение того, почему @cletus не прав.

Во-первых, реквизит для проведения исследования.

Во-вторых, вы делаете это неправильно.

Объяснение:

Исходный запрос:

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

Результат:

    "Seq Scan on blah a  (cost=0.00..145180063607.45 rows=2773807 width=4)"
    "  SubPlan"
    "    ->  Aggregate  (cost=52339.61..52339.63 rows=1 width=0)"
    "          ->  Seq Scan on blah  (cost=0.00..52339.59 rows=10 width=0)"
    "                Filter: (parentid = $0)"

Что происходит, когда вы заключаете в «выберите количество (1)»:

EXPLAIN SELECT count(1) FROM (
SELECT ID, (SELECT COUNT(1) FROM Blah WHERE ParentID= a.ID) as ChildCount
FROM Blah a) as bar
    "Aggregate  (cost=52339.59..52339.60 rows=1 width=0)"
    "  ->  Seq Scan on blah a  (cost=0.00..45405.07 rows=2773807 width=0)"

Заметили разницу?

Оптимизатор достаточно умен, чтобы понять, что ему не нужно выполнять подзапрос. Так что дело не в том, что коррелированные подзапросы работают быстро; дело в том, что НЕ ДЕЛАТЬ ИХ быстро :-).

К сожалению, он не может сделать то же самое для левого внешнего соединения, поскольку количество результатов не определяется заранее первым сканированием.

Урок № 1. Планы запросов говорят вам чертовски много. Плохой план эксперимента приведет к неприятностям.

Урок 1.1. Если вам не нужно выполнять объединение, ни в коем случае не делайте этого.

Я создал тестовый набор данных примерно из 2,7 миллиона запросов.

Левое внешнее соединение — без оболочки — на моем ноутбуке выполнялось 171 757 мс.

Коррелированный подзапрос... Я обновлю, когда он завершится, у меня 700 КБ мс, и он все еще работает.

Урок № 2. Когда кто-то говорит вам посмотреть на план запроса и утверждает, что он показывает алгоритмический порядок различий... посмотрите на план запроса.

person SquareCog    schedule 26.01.2009
comment
Я убил коррелированный подзапрос после того, как он выполнялся в течение 50 минут без ответа. Имейте в виду, что левое внешнее соединение выполняется чуть менее 3 минут. - person SquareCog; 26.01.2009
comment
-1 На самом деле я проверял обе версии (с оберткой COUNT(1) и без нее), и длительность была примерно одинаковой. Есть разница между измерением фактических результатов и теоретическим планом объяснения. - person cletus; 26.01.2009
comment
Не могли бы вы опубликовать время выполнения и объяснить планы для обоих запросов? Возможно, MySQL делает что-то умное, чего не делает Postgres, на котором я это запускал. Маловероятно, учитывая характер этих двоих, но возможно.. - person SquareCog; 26.01.2009
comment
MySQL может быть менее умным, чем SQL Server, когда дело доходит до принятия решения о необходимости выполнения подзапросов. Хотя я это проверил. Я пытался заставить свой SQL Server Express работать и импортировать большой объем данных, и это был кошмар, но я тоже опубликую это, если/когда он когда-нибудь заработает. - person cletus; 26.01.2009

Вы когда-нибудь пытались добавить индекс к родительскому идентификатору для MySQL. Я почти уверен, что время выполнения значительно улучшится. Не проверял, но я бы сказал, что MySQL просматривает все строки, чтобы определить количество. Это означает, что он выполняет 10-40 миллиардов (количество строк в таблице * 10000) запросов за эти 59 секунд.

Предположим, что SQL Server и Oracle создают индекс на лету. Если они это сделают, это будет только от 1 до 4 миллионов.

person gunter sammet    schedule 03.09.2010

Все ваши запросы предполагают, что порядок ввода родительских дочерних узлов является последовательным. Если в конце вводится потомок от одного из первых узлов и его ID или PK выше, то запрос не работает.

person Mark deraeve    schedule 02.02.2010