Зберігання парсингу: JSONB, upsert і GIN-індекс у PostgreSQL

Парсинг сайтів часто перетворюється на хаос: дублікати, повільні запити та втрата історії змін. Ми розробляємо схему зберігання на PostgreSQL з JSONB та upsert-логікою, яка наводить лад у даних. Наша команда реалізує проєкт під ключ — від проєктування до впровадження, забезпечуючи стабільну роботу та подальшу підтримку.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Зберігання парсингу: JSONB, upsert і GIN-індекс у PostgreSQL
Простий
від 1 дня до 3 днів
Часті запитання

Ми реалізували схему зберігання результатів парсингу для інтернет-магазину з 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);

Етапи проєктування схеми зберігання

  1. Аналіз домену. Визначаємо, які дані потрібні для вітрини: ціни, залишки, характеристики. З'ясовуємо, які поля обов'язкові, а які варіативні.
  2. Проєктування схеми. Спільні поля (ціна, назва, артикул) виносимо в окремі колонки. Інші пакуємо в JSONB-колонку data. Це дає гнучкість без втрати продуктивності.
  3. Реалізація upsert-логіки. Пишемо INSERT ... ON CONFLICT DO UPDATE. Ключ унікальності — (site_id, external_id). Це гарантує дедуплікацію при кожному обході.
  4. Індексація. GIN-індекс на data для швидких запитів за будь-яким атрибутом. B-tree на site_id та external_id для прискорення з'єднань.
  5. Тестування та оптимізація. Завантажуємо 100 000 записів, вимірюємо час INSERT і SELECT. Досягаємо <100 мс на типові запити.
  6. Документація та навчання. Передаємо команді замовника опис схеми та приклади запитів. Проводимо воркшоп.

Чому 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 днів. Зв'яжіться з нами, щоб оцінити ваш проєкт. Отримайте консультацію з проєктування схеми для вашого проєкту. Ми допоможемо уникнути типових помилок і прискорити розробку.

Наші компетенції:

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1394
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1369
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1142
  • Розробка веб-сайту для компанії ФІКСПЕР
    Розробка веб-сайту для компанії ФІКСПЕР
    1060