From 03e7f46cdb396b49e728eea1ad86af2f5d0d445e Mon Sep 17 00:00:00 2001 From: Sayakhfarov_RH Date: Mon, 9 Feb 2026 18:36:51 +0300 Subject: [PATCH 1/2] init --- README.md | 61 ++++++++++++++++++++ schemas/core.sql | 131 +++++++++++++++++++++++++++++++++++++++++++ schemas/examples.sql | 0 3 files changed, 192 insertions(+) create mode 100644 schemas/core.sql create mode 100644 schemas/examples.sql diff --git a/README.md b/README.md index 9f1b0b6..3536832 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,63 @@ ### Проектирование схемы БД +Поставленную задачу можно решить 3 основными подходами: Closure Table, Nested Sets, Adjacency List. + +Я выбрал Closure Table (таблица замыканий), так как он обеспечивает простые и +быстрые запросы к поддеревьям категорий независимо от глубины вложенности. +В любом запросе к дереву я могу обойтись обычным JOIN по таблице связей +category_closure, без рекурсивных CTE и сложной логики в SQL. Это даёт +предсказуемую производительность на больших иерархиях и хорошо масштабируется +при росте количества уровней и категорий. Дополнительно, Closure Table +позволяет так же просто получать не только потомков, но и всех предков +узла (например, для хлебных крошек) через тот же механизм. Структура +данных при этом остаётся реляционной и хорошо индексируемой: по предку +`ancestor_id` и по потомку `descendant_id`. Операции изменения структуры +(добавление, перемещение, удаление узлов) сложнее, чем при простом `parent_id`, +но их можно инкапсулировать в функции/процедуры и вызывать как единый API. +В реальном каталоге товаров такие изменения происходят значительно реже, чем +чтение каталога и выборка товаров по разделам, поэтому увеличение стоимости +изменений логично обменять на ускорение чтения. + +Таким образом, Closure Table лучше всего соответствует требованиям: +- произвольная глубина дерева, +- быстрый доступ к поддеревьям +- приемлемая стоимость редких операций изменения структуры категорий. + +--- +#### Диаграмма БД +Схема базы данных + +--- + +### Примеры данных в таблицах + +#### Покупатели + +Таблица покупателей + +--- + +#### Заказы +Таблица заказов + +--- + +#### Позиции заказа +Таблица позиций заказа + +--- + +#### Номенклатура (товары) +Таблица товаров + +--- + +#### Категории +Таблица категорий + +--- + +#### Closure Table для категорий +Closure Table категорий + +--- diff --git a/schemas/core.sql b/schemas/core.sql new file mode 100644 index 0000000..203c036 --- /dev/null +++ b/schemas/core.sql @@ -0,0 +1,131 @@ + +CREATE SCHEMA IF NOT EXISTS shop; + +------------------------------------------------------------ +-- 1. Таблицы-справочники и основные сущности +------------------------------------------------------------ + +-- 1.1. Клиенты +CREATE TABLE shop.client ( + id BIGSERIAL PRIMARY KEY, + name TEXT NOT NULL, + address TEXT +); + +COMMENT ON TABLE shop.client IS 'Таблица клиентов магазина'; + +COMMENT ON COLUMN shop.client.id IS 'Уникальный идентификатор клиента'; +COMMENT ON COLUMN shop.client.name IS 'Имя или наименование клиента'; +COMMENT ON COLUMN shop.client.address IS 'Адрес клиента'; + +------------------------------------------------------------ +-- 1.2. Категории (основная таблица) +------------------------------------------------------------ + +CREATE TABLE shop.category ( + id BIGSERIAL PRIMARY KEY, + parent_id BIGINT REFERENCES shop.category(id) ON DELETE SET NULL, + name TEXT NOT NULL +); + +CREATE INDEX idx_category_parent_id ON shop.category(parent_id); + +COMMENT ON TABLE shop.category IS 'Таблица категорий товаров (иерархическая структура)'; + +COMMENT ON COLUMN shop.category.id IS 'Уникальный идентификатор категории'; +COMMENT ON COLUMN shop.category.parent_id IS 'Ссылка на родительскую категорию (NULL для корневых категорий)'; +COMMENT ON COLUMN shop.category.name IS 'Наименование категории'; + +------------------------------------------------------------ +-- 1.3. Closure Table для категорий +------------------------------------------------------------ + +CREATE TABLE shop.category_closure ( + ancestor_id BIGINT NOT NULL REFERENCES shop.category(id) ON DELETE CASCADE, + descendant_id BIGINT NOT NULL REFERENCES shop.category(id) ON DELETE CASCADE, + depth INT NOT NULL, + PRIMARY KEY (ancestor_id, descendant_id) +); + +-- Быстрый поиск всех потомков (по предку) +CREATE INDEX idx_category_closure_ancestor_depth + ON shop.category_closure (ancestor_id, depth); + +-- Быстрый поиск всех предков (по потомку) +CREATE INDEX idx_category_closure_descendant_depth + ON shop.category_closure (descendant_id, depth); + +COMMENT ON TABLE shop.category_closure IS 'Closure Table для эффективной работы с иерархией категорий'; + +COMMENT ON COLUMN shop.category_closure.ancestor_id IS 'ID категории-предка в иерархии'; +COMMENT ON COLUMN shop.category_closure.descendant_id IS 'ID категории-потомка в иерархии'; +COMMENT ON COLUMN shop.category_closure.depth IS 'Глубина связи: 0 - сам себе предок, 1 - прямой потомок, 2+ - непрямой потомок'; + +------------------------------------------------------------ +-- 1.4. Номенклатура (товары) +------------------------------------------------------------ + +CREATE TABLE shop.product ( + id BIGSERIAL PRIMARY KEY, + category_id BIGINT NOT NULL REFERENCES shop.category(id) ON DELETE RESTRICT, + name TEXT NOT NULL, + quantity NUMERIC(18,3) NOT NULL DEFAULT 0, + price NUMERIC(18,2) NOT NULL +); + +CREATE INDEX idx_product_category_id ON shop.product(category_id); + +COMMENT ON TABLE shop.product IS 'Таблица товаров (номенклатура)'; + +COMMENT ON COLUMN shop.product.id IS 'Уникальный идентификатор товара'; +COMMENT ON COLUMN shop.product.category_id IS 'Ссылка на категорию товара'; +COMMENT ON COLUMN shop.product.name IS 'Наименование товара'; +COMMENT ON COLUMN shop.product.quantity IS 'Количество товара на складе'; +COMMENT ON COLUMN shop.product.price IS 'Цена товара'; + + +------------------------------------------------------------ +-- 1.5. Заказы +------------------------------------------------------------ + +CREATE TABLE shop.customer_order ( + id BIGSERIAL PRIMARY KEY, + client_id BIGINT NOT NULL REFERENCES shop.client(id) ON DELETE RESTRICT, + order_date TIMESTAMP NOT NULL DEFAULT NOW(), + status TEXT NOT NULL DEFAULT 'NEW' +); + +CREATE INDEX idx_customer_order_client_id ON shop.customer_order(client_id); +CREATE INDEX idx_customer_order_order_date ON shop.customer_order(order_date); + +COMMENT ON TABLE shop.customer_order IS 'Таблица заказов клиентов'; + +COMMENT ON COLUMN shop.customer_order.id IS 'Уникальный идентификатор заказа'; +COMMENT ON COLUMN shop.customer_order.client_id IS 'Ссылка на клиента, сделавшего заказ'; +COMMENT ON COLUMN shop.customer_order.order_date IS 'Дата и время создания заказа (по умолчанию - текущее время)'; +COMMENT ON COLUMN shop.customer_order.status IS 'Статус заказа: NEW - новый, и другие возможные статусы'; + +------------------------------------------------------------ +-- 1.6. Позиции заказа +------------------------------------------------------------ + +CREATE TABLE shop.customer_order_item ( + id BIGSERIAL PRIMARY KEY, + order_id BIGINT NOT NULL REFERENCES shop.customer_order(id) ON DELETE CASCADE, + product_id BIGINT NOT NULL REFERENCES shop.product(id) ON DELETE RESTRICT, + quantity NUMERIC(18,3) NOT NULL, + price_at_time NUMERIC(18,2) NOT NULL, + -- один товар одна позиция в заказе + UNIQUE (order_id, product_id) +); + +CREATE INDEX idx_order_item_order_id ON shop.customer_order_item(order_id); +CREATE INDEX idx_order_item_product_id ON shop.customer_order_item(product_id); + +COMMENT ON TABLE shop.customer_order_item IS 'Таблица позиций (состав) заказов'; + +COMMENT ON COLUMN shop.customer_order_item.id IS 'Уникальный идентификатор позиции в заказе'; +COMMENT ON COLUMN shop.customer_order_item.order_id IS 'Ссылка на заказ, к которому относится позиция'; +COMMENT ON COLUMN shop.customer_order_item.product_id IS 'Ссылка на товар в позиции заказа'; +COMMENT ON COLUMN shop.customer_order_item.quantity IS 'Количество товара в позиции'; +COMMENT ON COLUMN shop.customer_order_item.price_at_time IS 'Цена товара на момент создания заказа (фиксируется при оформлении)'; diff --git a/schemas/examples.sql b/schemas/examples.sql new file mode 100644 index 0000000..e69de29 From aa3bf72177c42d70c4e2fe778454d7c188114e6a Mon Sep 17 00:00:00 2001 From: Sayakhfarov_RH Date: Mon, 9 Feb 2026 19:03:05 +0300 Subject: [PATCH 2/2] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D1=8F?= =?UTF-8?q?=D0=B5=D1=82=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/README.md b/README.md index 3536832..4814ab7 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,85 @@ category_closure, без рекурсивных CTE и сложной логик --- +#### Все товары в категории Бытовая техника (id = 1): +```sql +SELECT + p.* +FROM + product p +inner join + category_closure cc ON + cc.descendant_id = p.category_id +WHERE + cc.ancestor_id = 1 +ORDER BY + p.id +; +``` +``` text +id|category_id|name |quantity|price | +--+-----------+------------------------------+--------+--------+ + 1| 3|Стиральная машина LG 6kg | 10.000|35000.00| + 2| 3|Стиральная машина Samsung 7kg | 5.000|42000.00| + 3| 6|Холодильник однокамерный Beko | 7.000|28000.00| + 4| 7|Холодильник двухкамерный Bosch| 3.000|55000.00| + 5| 5|Телевизор Samsung 43" | 12.000|32000.00| + 6| 5|Телевизор LG 55" | 4.000|58000.00| +``` + +#### "Хлебные крошки" для категории Двухкамерные холодильники +``` +SELECT + c.* +FROM + category_closure cc +inner join + category c ON + c.id = cc.ancestor_id +WHERE + cc.descendant_id = 7 +ORDER BY + cc.depth desc +; +``` + +``` +id|parent_id|name | +--+---------+---------------+ + 1| |Бытовая техника| + 4| 1|Холодильники | + 7| 4|Двухкамерные | +``` + +#### Пример получения заказа +``` +SELECT + p.name as "Наименование" + , c."name" as "Категория" + , coi.quantity as "Кол-во" + , coi.price_at_time as "Цена" + , coi.quantity * coi.price_at_time as "Сумма" + , sum(coi.quantity * coi.price_at_time) over() as "Итого" +FROM + customer_order_item coi +inner join + product p on + p.id = coi.product_id +inner join + category c on + c.id = p.category_id +WHERE + coi.order_id = 3 +; +``` +``` +Наименование |Категория |Кол-во|Цена |Сумма |Итого | +-----------------------------+-----------------+------+--------+------------+------------+ +Стиральная машина Samsung 7kg|Стиральные машины| 1.000|42000.00| 42000.00000|186000.00000| +Моноблок HP 24" |Моноблоки | 2.000|72000.00|144000.00000|186000.00000| +``` + + ### Примеры данных в таблицах #### Покупатели