Compare commits
2 Commits
2ff192c9fe
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa3bf72177 | ||
|
|
03e7f46cdb |
140
README.md
140
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 лучше всего соответствует требованиям:
|
||||||
|
- произвольная глубина дерева,
|
||||||
|
- быстрый доступ к поддеревьям
|
||||||
|
- приемлемая стоимость редких операций изменения структуры категорий.
|
||||||
|
|
||||||
|
---
|
||||||
|
#### Диаграмма БД
|
||||||
|
<img src="https://storage.ooru.ru/web/db_design/schema.jpg" width="700" alt="Схема базы данных" />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Все товары в категории Бытовая техника (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|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Примеры данных в таблицах
|
||||||
|
|
||||||
|
#### Покупатели
|
||||||
|
|
||||||
|
<img src="https://storage.ooru.ru/web/db_design/client.jpg" width="450" alt="Таблица покупателей" />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Заказы
|
||||||
|
<img src="https://storage.ooru.ru/web/db_design/customer_order.jpg" width="500" alt="Таблица заказов" />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Позиции заказа
|
||||||
|
<img src="https://storage.ooru.ru/web/db_design/customer_order_item.jpg" width="480" alt="Таблица позиций заказа" />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Номенклатура (товары)
|
||||||
|
<img src="https://storage.ooru.ru/web/db_design/product.jpg" width="500" alt="Таблица товаров" />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Категории
|
||||||
|
<img src="https://storage.ooru.ru/web/db_design/category.jpg" width="400" alt="Таблица категорий" />
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### Closure Table для категорий
|
||||||
|
<img src="https://storage.ooru.ru/web/db_design/category_closure.jpg" width="400" alt="Closure Table категорий" />
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
131
schemas/core.sql
Normal file
131
schemas/core.sql
Normal file
@@ -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 'Цена товара на момент создания заказа (фиксируется при оформлении)';
|
||||||
0
schemas/examples.sql
Normal file
0
schemas/examples.sql
Normal file
Reference in New Issue
Block a user