diff --git a/README.md b/README.md index 9f1b0b6..4814ab7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,142 @@ ### Проектирование схемы БД +Поставленную задачу можно решить 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 лучше всего соответствует требованиям: +- произвольная глубина дерева, +- быстрый доступ к поддеревьям +- приемлемая стоимость редких операций изменения структуры категорий. + +--- +#### Диаграмма БД +Схема базы данных + +--- + +#### Все товары в категории Бытовая техника (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| +``` + + +### Примеры данных в таблицах + +#### Покупатели + +Таблица покупателей + +--- + +#### Заказы +Таблица заказов + +--- + +#### Позиции заказа +Таблица позиций заказа + +--- + +#### Номенклатура (товары) +Таблица товаров + +--- + +#### Категории +Таблица категорий + +--- + +#### 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