Привет, многоуважаемые читатели Хабра!

Как нынешний аналитик БД с прошлым пятилетним опытом в B2B и B2C сегментах телеком-провайдеров, я хотел бы осветить одну из "болей" абонентов - «архивные» тарифы. Абоненты годами сидят на устаревших и дорогих тарифах, не зная, что в их же доме уже давно доступны более выгодные предложения от конкурентов.

Чтобы решить эту проблему и обеспечить открытость рынка тарифов, я решил объединить свой опыт в телекоме с навыками программирования. Так родилась идея проекта по автоматизированному парсингу тарифов провайдеров.

В первой части цикла статей я расскажу об архитектуре базы данных, которую я спроектировал для этой задачи.


Зачем это нужно?

Миссия проекта — создать инструмент, который автоматически мониторит провайдеров, избавляя пользователей от ручного сравнения и помогая им находить оптимальные условия по тарифу.

Архитектура БД: схема «Звезда» на PostgreSQL

Проектирование я начал со структуры на PostgreSQL, используя схему «Звезда».

  • Таблица фактов: Таблица город_провайдер со связью города с провайдером.

  • Таблицы измерений: Таблицы с информацией о тарифах (тарифы), городах (города) и провайдерах (провайдеры).

Для удобства дальнейшей работы все первичные и внешние ключи созданы в виде именованных ограничений.

CONSTRAINT ограничение_для_pk_id PRIMARY KEY (id),
CONSTRAINT ограничение_для_fk_id FOREIGN KEY (id) REFERENCES (id),
CONSTRAINT uq_город_провайдер UNIQUE (город_id, провайдер_id) -- уникальность значений для таблицы связей

Управление историчностью и автоматизация

Чтобы отслеживать изменения данных (например, когда провайдер деактивирует старый тариф), в таблицах связей, провайдерах и тарифах ведется историчность изменений с помощью автоматических триггеров.

Каждая сущность (кроме городов) имеет временные метки start_at и end_at для отслеживания актуальности записи.

CREATE TABLE IF NOT EXISTS сущности (
 -- ...
 start_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- для сущности связи
 end_at TIMESTAMP DEFAULT NULL, -- NULL значит «действует до сих пор»
 is_active BOOLEAN DEFAULT TRUE
);

Функции автоматически управляют этими полями при обновлениях:

CREATE OR REPLACE FUNCTION функция_реагирующая_на_изменения()
RETURNS TRIGGER AS $$
BEGIN
-- При любом изменение менять дату updated_at
NEW.updated_at = CURRENT_TIMESTAMP;
-- Менять дату end_at при неактивном статусе
IF NEW.is_active = FALSE AND OLD.is_active = TRUE THEN
NEW.end_at = CURRENT_TIMESTAMP;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Реализована каскадная деактивация: при деактивации провайдера автоматически деактивируются все связанные с ним тарифы.

Базовые индексы для ускорения чтения:

CREATE INDEX CONCURRENTLY idx_город_провайдер_город_id ON город_провайдер (город_id);
CREATE INDEX CONCURRENTLY idx_ город_провайдер_провайдер_id ON город_провайдер (провайдер_id);
CREATE INDEX CONCURRENTLY idx_тарифы_город_провайдер_id ON тарифы (город_провайдеры_id); -- таблица с тарифами

Что дальше?

В следующей части я планирую рассказать о парсинге на Python vs Selenium.

Буду признателен за критику моей архитектуры БД и кода. Какие дополнения вы бы внесли в структуру SQL (ограничения, улучшение триггера, дополнительные индексы)?

Однажды наткнулся на статью о том, как кто-то реализовал игру DOOM полностью на SQL, стало интересно какие возможности этого языка. Насколько реально перевести весь бэкенд на SQL. Планирую применять Python для парсинга и записи данных в базу. Постепенно планирую заменять некоторые функции Python на SQL. Интересно, насколько можно заменить Python на SQL в реальных задачах пишите ваше мнение.

Комментарии (21)


  1. rPman
    07.01.2026 05:33

    я хотел бы осветить одну из "болей" абонентов - «архивные» тарифы. Абоненты годами сидят на устаревших и дорогих тарифах, не зная, что в их же доме уже давно доступны более выгодные предложения от конкурентов.

    Что? Я не помню ни дня, что бы архивный тариф опсоса (а я с ними со времен nokia 3310) был хуже по деньгам для клиента... единственное исключение, временные бонусы за переход между провайдерами или покупка симки.

    Пару раз я повелся и менял тариф на предлагаемый опсосом

    (больше, лучше, всего за +20р), и оба раза меня 'качественно обманывали',.. из последних помню теле2 за апгрейдил мой архив, добавив плюшек но удалив минимальный пакет sms (тогда все опсосы их предлагали, это замылило глаз и не увидел в описании этого), а так как отсылка sms (десятки в месяц и опсос это прекрасно понимал) мне все еще нужна была, в итоге мне приходилось до 100р докидывать к абонентке именно из-за них, а назад никак.

    Но в любом случае, каким бы плохим не был когда то сейчас уже архивный тариф, он все еще в разы дешевле любого доступного тарифа (есть 100р 'интернет вещей', но пока мобильный интернет не доломан, на него переходить не спешу).

    Единственные, кому архивные тарифы мешают - это сами операторы сотовой связи.

    p.s. как вы описываете тариф и главное как их сравниваете?


  1. shurutov
    07.01.2026 05:33

    Абоненты годами сидят на устаревших и дорогих тарифах, не зная, что в их же доме уже давно доступны более выгодные предложения от конкурентов.

    То ли плакать, то ли смеяться. Я знаю две возможности удешевить использование мобильной связи:

    1. индивидуальное предложение, при этом всё зависит от оператора;

    2. сменить оператора;

    А буковки про более дешёвые тарифы - это не просто эффективно-менеджерский рекламно-маркетоложеский поток букв без подачи мысли, это прямое введение в заблуждение клиентов, что является нарушением закона.


  1. Ninil
    07.01.2026 05:33

    DDL на создание 4х таблиц, гордо названный архитектурой - вот текущий уровень статей на хабре(


    1. shurutov
      07.01.2026 05:33

      Я лично не вижу DDL. Только фрагменты оного DDL. :(


      1. Ninil
        07.01.2026 05:33

        Ну вы поняли, что я имел в виду;)


        1. shurutov
          07.01.2026 05:33

          Но так-то да. :)
          Прошу прощения за то, что мой ответ выглядит резко. Захотелось уточнить, но более корректная форма этого ответа не пришла в голову по причине возмущения "качеством" материала.


  1. shurutov
    07.01.2026 05:33

    По технической части же - всё совсем грустно. Нет расширенного, хотя бы словесного, описания задачи, которая решается:

    • какие данные будут храниться для каждой из сущностей - тарифы, провайдеры, населённые пункты?

    • какую информацию будет возвращать ваше ПО?

    • в каком виде будет возвращать?

    И т.д., и т.п.
    Соответственно, нет полной структуры таблиц, что вызывает серьёзное удивление. Равно, как и кода, который будет создавать эти таблицы.
    Зачем триггер? Вы отправляете запрос провайдеру, который возвращает информацию о том, что тариф переведён в архив, соответственно, ваше ПО должно просто обновить соответствующую информацию в БД. безо всяких триггеров.

    Резюме. Материал не тянет даже на экзаменационный вопрос из курса проектирования баз данных для подготовки инженеров профильных специальностей, не говоря уже о чём-то более объёмном, начиная с лабораторных работ.


    1. MalblshProgrammist Автор
      07.01.2026 05:33

      Учту замечания, дополню, отредактирую статью. Парсер будет получать тарифы у провайдеров и проверять, существуют ли эти тарифы в базе данных. Если обнаруживаются какие-либо отличия в тарифе (например, отличается скорость или название тарифа), то данный тариф помечается как архивный, а триггер автоматически заполняет поле end_at, делая тариф неактивным.