SQL Parent / Child рекурсивный вызов или объединение? - PullRequest
5 голосов
/ 26 января 2009

Я не могу найти подходящий пример там.

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

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

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

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

3, col1text, col2text, 1 (child)
5, col1texta, col2texta, 2 (ребенок)
6, col1textb, col2textb, 0 (дочерний элемент)
9, col1textc, col2textc, 4 (child)

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

Ответы [ 5 ]

13 голосов
/ 26 января 2009

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

4 голосов
/ 26 января 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
2 голосов
/ 26 января 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)"

Что происходит, когда вы заключаете в "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
    "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 мс на моем ноутбуке.

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

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

1 голос
/ 03 сентября 2010

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

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

0 голосов
/ 02 февраля 2010

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

...