Получить большое дерево без накладных расходов в левых таблицах

88
6

Мой вопрос более теоретический, и речь идет о том, почему RDBMS/драйверы возвращают данные так, как они все это делают, а не то, как они находят правильный набор, и как его найти. Я хорошо знаком с SQL, но есть одна вещь, которая всегда раздражала мое чувство экономики.

Рассмотрим следующий граф "класса":

A {
field1, ..., field9
b_items = [ b1, ..., bN ]
}

B {
field1, ..., field6
c_items = [ c1, ..., cM ]
}

C {
field1, field2
}

У нас мало объектов A, каждый объект A имеет много объектов B, и каждый объект B имеет много объектов C. count(A) < count(B) << count(C).

Теперь я хотел бы использовать RDBMS для его хранения, потому что отношения крутые, а оптимизаторы - умные, поэтому я могу получить практически все за миллисекунды при условии, что есть хороший план и набор индексов.

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

SELECT *
FROM A
LEFT JOIN B ON B.a_id = A.id
LEFT JOIN C ON C.b_id = B.id
WHERE whatever

Сервер базы данных возвращает набор результатов, объединенный из всех столбцов из всех таблиц, правильно соединенных в сортировку дерева:

A.f1 .... A.f9  B.f1 .... B.f6  C.f1 C.f2
---------------------------------------------------
1 1 1 1 1 1 1 1
1 1 1 1 1 1 2 2
1 1 1 1 1 1 3 3
... more rows...
1 1 1 1 1 1 999 999

1 1 1 2 2 2 1 1
1 1 1 2 2 2 2 2
... more rows...
1 1 1 2 2 2 999 999
... lots of rows ...
1 1 1 99 99 99 999 999

2 2 2 -- oh there it is, A[2]
...
5 5 5 NULL NULL NULL NULL NULL -- A[5] has no b_items
...
9 9 9 ...

Проблема в том, что если A имеет много столбцов, особенно с текстом, json, другими тяжелыми данными, он дублируется тысячами раз, чтобы соответствовать каждому продукту объединения + B + C. Почему SQL-серверы по крайней мере просто не отправляют мне то же самое {A, B} -rows после первого в группе объединений? В идеале я хотел бы увидеть что-то подобное:

[
{
<A-fields>,
B = [
{
<B-fields>,
C = [
{
<C-fields>
},
... more C rows
]
},
... more B rows
]
},
... more A rows
]

который в значительной степени напоминает то, что мне действительно нужно получить в памяти на стороне клиента. Я знаю, что я могу сделать больше запросов для получения меньших данных, например, через A.id IN (ids...) или сохраненных proc, возвращающих нули в паразитных строках, но не реляционной модели, предназначенной для однократного доступа? Круглые поездки тяжелы, и так догадки планировщика. И реальные графики данных редко имеют только высоту в 3 шага (рассмотрите 5-10). Тогда почему бы не сделать все за один проход, но без чрезмерного трафика?

Я в порядке с повторяющимися ячейками в столбцах A и B, потому что обычно их не так много, но, возможно, мне не хватает чего-то основного, SQL и не-хаки, которое google скрывает от меня на протяжении многих лет.

Спасибо!

спросил(а) 2021-01-25T16:56:04+03:00 4 месяца, 3 недели назад
1
Решение
62

json_agg(), возможно, не самая быстрая вещь. Кроме того, мне интересно, будет ли ваш ORM правильно переваривать его и создать экземпляр правильных объектов.

Обычный способ - просто сделать:

SELECT ... FROM a WHERE ...

Затем вы восстанавливаете идентификаторы и делаете:

SELECT ... FROM b WHERE a_id IN (the list you just got)
SELECT ... FROM c WHERE a_id IN (the list you just got)

Они полностью генерируются ORM. Если ORM является умным, вы получаете один запрос за таблицу. Если это глупо, вы получаете один запрос на объект... Однако это заставляет три запроса, с круглыми кругами сети, а также с некоторой обработкой. К счастью, postgres дадут вам ваш торт и съедят его, хотя это требует немного дополнительной работы.

Таким образом, вы можете создать функцию в plpgsql, которая возвращает "SETOF refcursor". Поскольку refcursor является курсором, функция может возвращать несколько наборов результатов.

Пример.

Назад в тот день, когда я делал sql для веб-сайтов, я использовал это несколько раз. В основном, когда вы просто хотите получить один объект и несколько зависимостей, поэтому фактический синтаксический разбор и планирование запросов занимает больше времени, чем сами запросы, которые возвращают одну строку или несколько. Там он использует функцию, поэтому все уже скомпилировано. Это очень эффективно.

ответил(а) 2021-01-25T16:56:04+03:00 4 месяца, 3 недели назад
63

Единственный способ избежать дублирования передачи данных - использовать агрегатные функции, такие как string_agg() или array_agg(). Вы также можете агрегировать данные с помощью jsonb-функций. Вы даже можете получить один json-объект вместо табличных данных, например:

select jsonb_agg(taba)
from (
select to_jsonb(taba) || jsonb_build_object('tabb', jsonb_agg(tabb)) taba
from taba
left join (
select to_jsonb(tabb) || jsonb_build_object('tabc', jsonb_agg(to_jsonb(tabc))) tabb
from tabb
join tabc on tabc.bid = tabb.id
group by tabb.id
) tabb
on (tabb->>'aid')::int = taba.id
group by taba.id
) taba

Полный рабочий пример.

ответил(а) 2021-01-25T16:56:04+03:00 4 месяца, 3 недели назад
Ваш ответ
Введите минимум 50 символов
Чтобы , пожалуйста,
Выберите тему жалобы:

Другая проблема