Как организовать хранение и доступ к большому и иерархичному объекту в приложении
Что проверяют
Умеете ли вы идти от сценариев доступа: что читают чаще, поддерево или весь объект, как часто он меняется и какой он глубины. Следом обычно спрашивают, как получить всех потомков узла одним запросом и не упереться в N+1.
Как отвечать
Сначала уточните сценарии: объём, глубина, частота записи, нужны ли поддеревья и путь до корня. Дальше варианты хранения в базе:
- список смежности:
parent_idу каждого узла. Запись дешёвая, потомки достаются рекурсивным CTEWITH RECURSIVE. Подходит по умолчанию; - материализованный путь: строка вида
1.4.17, в PostgreSQL типltreeс GiST-индексом. Поддерево это один индексируемый запрос, но перенос ветки переписывает пути у всех потомков; - closure table: отдельная таблица пар «предок, потомок, глубина». Быстрые запросы в любую сторону ценой лишних строк;
- nested sets: границы
lft/rgt, чтение очень быстрое, но вставка перенумеровывает полдерева. Годится для редко меняющихся справочников; - JSONB-документ: если объект читается и пишется целиком, он хранится в одной колонке с GIN-индексом, частичное обновление через
jsonb_set.
Со стороны Python: в SQLAlchemy 2.0 модель объявляется через DeclarativeBase, у relationship для детей стоит lazy="selectin" или явная загрузка, иначе обход дерева порождает запрос на каждый узел. В памяти дерево удобно держать плоско: словарь {id: node} плюс индекс детей по parent_id, для экономии памяти @dataclass(slots=True). Обход делайте итеративно через стек или collections.deque, не рекурсией.
Для чтения горячих поддеревьев помогает кэш в Redis с инвалидацией по изменению ветки, а огромный JSON-файл читается потоково через ijson, без загрузки целиком.
Где спотыкаются
Сразу предлагают документную базу или «положить всё в JSON», не спросив, как объект меняется. Документ, который правят сотни раз в минуту, превращается в точку конкуренции.
Обходят дерево рекурсивной функцией. Лимит рекурсии в CPython по умолчанию 1000, глубокая ветка упадёт с RecursionError.
Грузят весь объект ради одного узла и получают секунды на запрос и сотни мегабайт памяти на каждый воркер.