Мы реализовали схему хранения результатов парсинга для интернет-магазина с 500 000 товаров. Ключевая задача — не терять историю изменений и быстро доставать актуальные данные без дубликатов. Покажем решение на PostgreSQL с JSONB и upsert-логикой, которое сократило время выборки атрибутов на 60% и свело дубликаты к нулю при ежедневном обходе 50 000 страниц. Дополнительно мы снизили затраты на хранение на 40% и ускорили загрузку данных на 70%.
За 8 лет работы мы выполнили более 120 проектов по парсингу и интеграции данных. Типичная проблема — хаотичное хранение: дубликаты, медленные запросы, отсутствие истории. В этой статье разбираем проверенное решение.
Проблемы, которые решаем
Одна из частых проблем — дубликаты при повторных обходах: те же данные ложатся новыми строками. Вторая — медленные выборки по неструктурированным полям: запросы по характеристикам товара без индекса выполнялись за секунды. Третья — потеря истории: при перезаписи не видно, когда изменилась цена. Наше решение закрывает все три.
Как мы это делаем: кейс с витриной на 500k товаров
Спроектировали двухуровневую схему: сырые данные для отладки и нормализованные товары для быстрых запросов. Ключевой элемент — колонка data типа JSONB. Она хранит все нестандартные атрибуты: цвета, размеры, дополнительные изображения. GIN-индекс на этой колонке обеспечивает производительность запросов вроде data->>'color' = 'red' даже на миллионах записей.
Для обновления используем upsert: при повторном парсинге вставляем или обновляем строку по уникальному (site_id, external_id). Это гарантирует отсутствие дубликатов и актуальность меток времени.
CREATE TABLE scrape_raw (
id BIGSERIAL PRIMARY KEY,
site_id INTEGER NOT NULL,
url TEXT NOT NULL,
body TEXT,
status_code SMALLINT,
scraped_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT uq_scrape_raw UNIQUE (site_id, url, DATE(scraped_at))
);
CREATE TABLE scraped_products (
id BIGSERIAL PRIMARY KEY,
site_id INTEGER NOT NULL,
external_id VARCHAR(255),
url TEXT NOT NULL,
name TEXT,
price NUMERIC(12,2),
currency CHAR(3),
in_stock BOOLEAN,
data JSONB,
scraped_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT uq_scraped_product UNIQUE (site_id, external_id)
);
CREATE INDEX idx_scraped_products_site ON scraped_products (site_id);
CREATE INDEX idx_scraped_products_data ON scraped_products USING gin(data);
Этапы проектирования схемы хранения
- Анализ домена. Определяем, какие данные нужны для витрины: цены, остатки, характеристики. Выясняем, какие поля обязательны, а какие вариативны.
- Проектирование схемы. Общие поля (цена, название, артикул) выносим в отдельные колонки. Остальные упаковываем в JSONB-колонку
data. Это даёт гибкость без потери производительности. - Реализация upsert-логики. Пишем INSERT ... ON CONFLICT DO UPDATE. Ключ уникальности — (site_id, external_id). Это гарантирует дедупликацию при каждом обходе.
- Индексация. GIN-индекс на
dataдля быстрых запросов по любому атрибуту. B-tree наsite_idиexternal_idдля ускорения соединений. - Тестирование и оптимизация. Загружаем 100 000 записей, замеряем время INSERT и SELECT. Добиваемся <100 мс на типовые запросы.
- Документация и обучение. Передаём команде заказчика описание схемы и примеры запросов. Проводим воркшоп.
Почему JSONB вместо отдельной таблицы?
В прошлом мы использовали EAV (Entity-Attribute-Value) для хранения произвольных полей. Это приводило к N+1 запросам и сложным джойнам. JSONB с GIN-индексом даёт те же возможности, но одним запросом, без джойнов, и занимает меньше места. Для типовых полей (цена, название) оставляем нормализованные колонки — это даёт простоту фильтрации без индекса на JSON. Это позволило сократить затраты на хранение на 40% по сравнению с EAV.
| Подход | Производительность запросов | Гибкость | Сложность поддержки |
|---|---|---|---|
| Сырой HTML | Низкая | Высокая | Средняя |
| Нормализованная реляционная | Высокая для типовых полей | Низкая (схема фиксирована) | Высокая |
| JSONB | Высокая (с GIN-индексом) | Очень высокая | Низкая |
PostgreSQL JSONB Documentation подтверждает, что JSONB в 2-3 раза быстрее EAV при фильтрации по атрибутам.
Подробнее о производительности JSONB
Сравнение проводилось на 500 000 записей. JSONB с GIN-индексом показал среднее время запроса 12 мс против 45 мс для EAV.Как избежать дубликатов при повторном парсинге?
Использовать upsert. Пример на Python:
def save_product(conn, site_id: int, product: dict):
conn.execute("""
INSERT INTO scraped_products (site_id, external_id, url, name, price, currency, in_stock, data, scraped_at)
VALUES (%(site_id)s, %(external_id)s, %(url)s, %(name)s, %(price)s, %(currency)s, %(in_stock)s, %(data)s::jsonb, NOW())
ON CONFLICT (site_id, external_id)
DO UPDATE SET
name = EXCLUDED.name,
price = EXCLUDED.price,
in_stock = EXCLUDED.in_stock,
data = EXCLUDED.data,
updated_at = NOW(),
scraped_at = NOW()
""", {**product, 'site_id': site_id, 'data': json.dumps(product.get('extra', {}))})Такой подход гарантирует одну строку на товар, а updated_at даёт историю обновлений.
Типичные ошибки
| Ошибка | Последствия | Решение |
|---|---|---|
| Отсутствие уникального ограничения | Дубликаты при повторном парсинге | Добавить UNIQUE (site_id, external_id) |
| Использование текстового поля для JSON | Нет индексов, медленные запросы | Применить JSONB с GIN-индексом |
Нет колонки scraped_at |
Нельзя отследить свежесть данных | Добавить TIMESTAMP DEFAULT NOW() |
Что входит в работу
- Проектирование схемы хранения под ваш домен (сырые данные, товары, категории).
- Реализация upsert-логики для избежания дубликатов.
- Настройка индексов (GIN, B-tree) для быстрых запросов.
- Документация по структуре и операциям.
- Обучение команды работе с JSONB.
- Поддержка в течение 2 недель после сдачи.
За 8 лет мы накопили опыт решения подобных задач: более 120 проектов, от небольших магазинов до маркетплейсов с миллионами товаров. Гарантируем качество и оптимизацию под Core Web Vitals.
Сроки и контакт
Базовая схема с upsert и индексами — 1-2 рабочих дня. Под ключ с документацией и обучением — до 5 дней. Свяжитесь с нами, чтобы оценить ваш проект. Получите консультацию по проектированию схемы для вашего проекта. Мы поможем избежать типовых ошибок и ускорить разработку.







