PostgreSQL как мозг системы управления 16 виллами на Бали

Представьте: 16 вилл разбросаны по Убуду, Чангу и Семиньяку, гости заезжают и выезжают каждый день, персонал получает задачи через Telegram-бота, финансовый мониторинг работает в автоматическом режиме, а семь AI-агентов параллельно обрабатывают лиды, рассылают объявления и генерируют отчёты. Всё это держится на одном фундаменте — PostgreSQL. Не Firebase, не Airtable, не Google Sheets. PostgreSQL — реляционная база данных, которая обрабатывает 280 000 запросов в день и ни разу не подвела.

С чего всё началось: пять таблиц и хаос в WhatsApp

Когда портфель вилл Solar Property перевалил за десять объектов, стало очевидно: управлять бизнесом в Google Sheets — это не масштабировать, а тушить пожары. У каждого менеджера была своя таблица. Расходы по одной вилле жили в одном файле, бронирования — в другом, контракты с хозяевами — в третьем. Бухгалтер в конце месяца открывал это всё и методично сходил с ума.

Менеджер из Убуда не видел, что происходит в Чангу. Если гость отменял бронь, таблица обновлялась вручную — а иногда не обновлялась совсем. Переписки в WhatsApp превратились в «базу знаний»: информацию о конкретной вилле искали прокруткой чатов вверх. Это работало при пяти виллах. При десяти — начало трещать. При шестнадцати — рухнуло бы окончательно.

Самое болезненное — рассинхронизация данных. Один менеджер обновил расходы, другой не обновил общую сводку. Цифры не сходились. Я тратил полдня, чтобы найти расхождение в 200 000 рупий, и не всегда находил причину. Решение было одно: перенести всё в единую систему с одним источником правды.

Почему PostgreSQL, а не что-то попроще

Вопрос «почему именно PostgreSQL» я слышу регулярно. Люди привыкли к Airtable, Firebase, Notion-базам. Отвечаю честно и по пунктам.

ACID-транзакции и надёжность

ACID — это Atomicity, Consistency, Isolation, Durability. По-русски: транзакция либо выполняется полностью, либо не выполняется вообще. Если бронирование создаётся — оно создаётся с платежом, статусом и задачей на уборку одновременно. Не по частям. Не «сохранили бронь, а платёж завис». PostgreSQL гарантирует консистентность данных на уровне ядра.

Firebase — документная база без строгих транзакций на уровне нескольких коллекций. Airtable — красивый интерфейс, но с ограничениями на количество записей и без настоящего SQL. Google Sheets тормозит на 5 000 строк и падает при одновременном редактировании.

JSONB для гибких данных

Каждая вилла уникальна. Одна имеет частный бассейн, другую — нет. У одной пять спален, у другой — две. Одна принимает животных, другая нет. Список характеристик постоянно расширяется: добавляем новое поле — и не хочется менять схему таблицы ради одного атрибута.

PostgreSQL решает это через тип данных JSONB. Поле amenities в таблице villas хранит произвольный JSON-объект: бассейн, кондиционер, кухня, вид на рисовые поля, близость к пляжу. Добавить новую характеристику — это изменение в данных, не в схеме. При этом по JSONB-полям можно делать индексы и полноценные запросы. Это сочетание гибкости документной базы с мощью реляционной.

Расширяемость и экосистема

PostgreSQL поддерживает PostGIS для геоданных — в будущем это позволит строить запросы типа «найди все виллы в радиусе 5 км от пляжа Куты». Поддерживает полнотекстовый поиск, оконные функции, материализованные представления. Это не просто хранилище — это вычислительный движок для аналитики.

И главное: PostgreSQL бесплатный, работает на нашем сервере, никаких лимитов на записи, никакой зависимости от стороннего SaaS. Полный контроль.

Архитектура базы: что и где хранится

Вся система Solar Property описывается восемью основными таблицами. Каждая отвечает за свой домен данных, все связаны через внешние ключи.

-- Схема Solar Property (упрощённо)

villas (16 записей)
├── id, name, location, capacity
├── price_per_night, currency
├── amenities JSONB          -- гибкие характеристики
├── status (active/inactive)
└── owner_id → contacts

bookings (8 000+)
├── villa_id → villas
├── guest_id → guests
├── check_in, check_out
├── status (confirmed/cancelled/pending)
├── source (airbnb/booking/direct/whatsapp)
├── total_amount, currency
└── channel_commission

guests (2 500+)
├── name, email, phone
├── nationality, language
├── booking_history JSONB
└── preferences JSONB

tasks (4 000+)
├── villa_id → villas
├── type (cleaning/repair/checkin/checkout)
├── assigned_to, due_date
├── status (pending/in_progress/done)
└── created_by (bot/human)

leads (постоянно растёт)
├── source (whatsapp/instagram/airbnb/direct)
├── contact_info JSONB
├── villa_interest → villas
├── status (new/qualified/converted/lost)
└── ai_score (оценка от 0 до 100)

finances (5 000+)
├── villa_id → villas
├── type (income/expense)
├── category, amount, currency
├── date, description
└── verified BOOLEAN

ai_logs (десятки тысяч)
├── agent_name, model
├── tokens_input, tokens_output
├── cost_usd, duration_ms
├── task_type, status
└── created_at

Восемь таблиц. Всё в одном месте. Один SQL-запрос — и я вижу полную картину по любой вилле за любой период. Больше не нужно открывать пять табличек и складывать числа в голове.

Таблица villas: центр схемы

Таблица villas — это якорь всей системы. Каждая из 16 вилл — одна строка. Но простота обманчива: поле amenities типа JSONB хранит десятки атрибутов в структурированном JSON. Запрос «найти все виллы с бассейном и видом на рисовые поля вместимостью от четырёх человек» выполняется за миллисекунды благодаря GIN-индексу по JSONB-полю.

Таблица bookings: сердце операционки

Бронирования приходят из четырёх источников: Airbnb, Booking.com, прямые бронирования через сайт и WhatsApp-лиды. Поле source фиксирует канал. Это критически важно для аналитики: мы точно знаем, сколько приносит каждый канал, какова комиссия и какова реальная маржа. Подробнее о синхронизации каналов бронирования — в отдельной статье.

Таблица leads: воронка лидов под контролем AI

Каждый потенциальный гость, который написал в WhatsApp, оставил заявку через Instagram или отправил enquiry на Airbnb — попадает в таблицу leads. AI-агент автоматически оценивает каждый лид по шкале от 0 до 100 на основе явного интереса, бюджета, дат и других сигналов. Горячие лиды с оценкой выше 70 уходят менеджеру немедленно. Как устроена эта система — читайте здесь.

Таблица ai_logs: контроль над AI-расходами

Каждый вызов языковой модели — будь то Claude, GPT-4 или Gemini — логируется в ai_logs. Токены, стоимость в долларах, время выполнения, название агента. Это позволяет точно считать ROI каждого AI-агента: сколько стоит одна обработанная бронь, один сгенерированный отчёт, один отработанный лид. Как мы оптимизируем стоимость AI — отдельный материал.

Триггеры и функции: автоматика внутри базы

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

Триггер на создание бронирования

Когда в таблицу bookings добавляется новая запись со статусом confirmed, триггер автоматически создаёт три задачи: уборку за день до заезда, встречу гостей в день заезда и уборку после выезда. Менеджер ничего не делает вручную — задачи появляются сами, назначаются по расписанию и уведомление уходит исполнителю через Telegram-бота.

Это убирает целый класс ошибок: забытые уборки, незаланированные встречи, накладки по времени. База знает о бронировании — база сама формирует операционную цепочку.

Функция расчёта occupancy rate

Occupancy rate — загрузка вилл — это ключевой показатель в property management. Считать его вручную каждый раз неудобно. Мы написали PostgreSQL-функцию, которая принимает villa_id и диапазон дат и возвращает точный процент загрузки с учётом отменённых бронирований, технических простоев и подтверждённых броней.

Именно эта функция однажды показала 963% загрузки в начале апреля. Дашборд не врал — врала логика вызова: 183 забронированные ночи делились на один прошедший день месяца вместо общего числа дней. Один запрос выявил баг, один запрос его подтвердил, один коммит исправил. Реальная загрузка оказалась 32% — нормальный уровень для начала месяца.

Функция revenue per villa

Ещё одна ключевая функция — расчёт дохода по вилле за период с учётом комиссий каналов бронирования. Airbnb берёт одну комиссию, Booking.com — другую, прямые бронирования идут без комиссии. Функция автоматически применяет правильный коэффициент в зависимости от поля source в таблице бронирований и возвращает чистую выручку.

Row-level security для мультитенантности

Solar Property планирует масштабирование: подключение новых управляющих компаний и партнёров. Для этого в PostgreSQL настроена Row-level security (RLS) — механизм, при котором каждый пользователь видит только свои данные на уровне строк. Менеджер одного пула вилл физически не может получить данные другого пула, даже при прямом SQL-запросе. Безопасность без дополнительного кода в приложении.

Все AI-агенты работают через одну базу

PostgreSQL — это не просто хранилище данных. Это общая память всей AI-системы. Семь агентов читают и пишут в одну и ту же базу, видят одну и ту же картину мира.

Голосовой ассистент Алиса

Когда гость звонит и спрашивает о доступности вилл на конкретные даты, Алиса делает SELECT в таблицу bookings в реальном времени. Ответ — актуальный, не из кэша, не из вчерашнего экспорта. Если вилла только что заблокирована другим запросом — Алиса это знает и не предложит её гостю. Подробнее о голосовом ассистенте читайте здесь.

Рассыльщик вилл

Broadcaster — агент, который публикует объявления о виллах в 143 Telegram-группы через 9 аккаунтов — берёт из базы актуальную информацию о каждой вилле перед каждой публикацией. Цена, доступность, фото. Если цена изменилась — в следующем посте уже новая цена.

Именно с этим агентом произошла показательная история. Алгоритм выбора следующей задачи делал 280 000 отдельных запросов к базе за один вызов. Формула простая: 44 виллы × 143 группы × 9 аккаунтов × 5 запросов. Сервер загружался на 27% CPU только на этой операции, рассыльщик молчал пять дней. После переписки на батчевые запросы — 10 запросов вместо 280 000, нагрузка упала до 1.6%, первый пост ушёл через восемь минут после деплоя. Детальный разбор этой оптимизации — в отдельной статье.

Мониторинг финансов

Каждое утро финансовый агент делает агрегационный запрос по таблице finances: суммирует доходы и расходы за предыдущий день с разбивкой по виллам, сравнивает с аналогичным периодом прошлого месяца и флагирует аномалии. Если расход на электричество по конкретной вилле вырос в два раза — агент пришлёт алерт в Telegram до того, как придёт счёт за месяц. Как работает автоматический мониторинг финансов.

Монитор лидов

Агент сканирует WhatsApp-группы и Instagram в поисках потенциальных гостей. Найденный лид немедленно записывается в таблицу leads с источником, контактом и первичной оценкой. Дублей нет: перед записью проверяется уникальность по номеру телефона. История взаимодействий хранится в JSONB-поле — можно посмотреть весь путь клиента от первого сообщения до бронирования.

Планировщик задач

Централизованный планировщик задач читает таблицу tasks и распределяет задачи по исполнителям на основе ключевых слов и типа. Задачи по виллам — одному агенту, финансы — другому, контент — третьему. Автоназначение работает через PostgreSQL-функцию, которая анализирует поле type и метаданные задачи. О централизованном планировщике задач — подробнее здесь.

Индексы и оптимизация: 280 000 запросов в день

280 000 запросов в день — это не гипотетическая нагрузка. Это реальные цифры после подключения всех агентов и рассыльщика. Без правильных индексов база давно бы легла.

Индексы по часто используемым полям

В таблице bookings индексированы: villa_id, check_in, check_out, status, source. Практически любой аналитический запрос по бронированиям использует один или несколько из этих столбцов. Составные индексы на (villa_id, check_in, check_out) ускоряют проверку доступности виллы на конкретные даты — самый частый запрос в системе.

В таблице leads индекс по source и status позволяет быстро выбирать необработанные лиды из конкретного канала. В таблице ai_logs индекс по created_at и agent_name обеспечивает быстрые отчёты по расходам за период.

GIN-индексы для JSONB

По полям типа JSONB стандартные B-tree индексы не работают. PostgreSQL предлагает GIN (Generalized Inverted Index) — специальный тип для полнотекстового поиска и JSONB. GIN-индекс по полю amenities в таблице villas позволяет делать запросы вида «найди все виллы с конкретным атрибутом» за миллисекунды даже при росте числа атрибутов.

Партиционирование для больших таблиц

Таблица ai_logs растёт быстрее всего — каждый вызов AI-агента добавляет строку. Для неё настроено партиционирование по месяцам: данные за каждый месяц хранятся в отдельной партиции. Это позволяет быстро удалять старые данные и ускоряет запросы за конкретный период. Запрос «расходы на AI за апрель» читает только одну партицию вместо всей таблицы.

Бэкапы и отказоустойчивость

Хранить всё в одной базе данных — это риск ровно настолько, насколько ненадёжны ваши бэкапы. Мы пришли к этому пониманию болезненным путём.

История с потерей данных

На раннем этапе внедрения бэкапы настроили «потом». Неудачное обновление скрипта привело к потере данных за два дня — примерно 400 записей. Часть удалось восстановить вручную из переписок, часть — нет. Это был болезненный, но ценный урок: бэкапы должны быть с первого дня, не когда «всё устаканится».

Текущая система бэкапов

Сейчас настроено три уровня защиты данных:

  • pg_dump каждые 6 часов — полный дамп базы сохраняется на отдельный сервер. Четыре снимка в день, хранятся 30 дней. Одна строка в crontab, ноль нервов.
  • Point-in-time recovery (PITR) — WAL-логи (Write-Ahead Log) непрерывно реплицируются. При необходимости базу можно восстановить на любой момент времени с точностью до транзакции. Не «состояние на 6 утра», а «состояние в 14:37:22».
  • Мониторинг бэкапов — отдельный агент проверяет, что последний бэкап не старше восьми часов. Если бэкап не обновился — приходит алерт. Бэкап без мониторинга — это иллюзия безопасности.

Тестирование восстановления

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

Интеграции: как данные попадают в базу

PostgreSQL — центр, но данные поступают из множества источников. Каждая интеграция решалась по-своему.

Синхронизация с channel manager

Бронирования из Airbnb и Booking.com поступают через eZee PMS — channel manager, который агрегирует все каналы. Вебхук от eZee обрабатывается Python-сервисом, который трансформирует данные и записывает в таблицу bookings. Дубли предотвращаются через уникальный constraint на поле external_id (идентификатор брони в исходном канале). Подробнее о синхронизации с Airbnb и Booking.com.

WhatsApp и Telegram как источники лидов

Каждое сообщение от потенциального гостя в WhatsApp обрабатывается агентом, который классифицирует намерение и при наличии интереса к аренде создаёт запись в leads. Telegram-боты для персонала пишут в таблицу tasks напрямую через API. Все операции записи проходят через транзакции — частичных состояний нет.

Дашборд и отчёты

Ежедневный дашборд — это набор SQL-запросов к PostgreSQL, которые запускаются каждое утро в 8:00 и отправляют результат в Telegram. Без промежуточных сервисов, без кэшей, без устаревших данных. Один запрос — одна цифра — одно решение. Управление без совещаний, без ручных сводок, без «уточним позже». 10 ежедневных проверок бизнеса через AI — читайте здесь.

Уроки внедрения: что пошло не так и как это исправить

Переход от Excel-хаоса к единой базе данных — не быстрый процесс. Вот конкретные ошибки, которые мы совершили, и как их избежать.

Ошибка 1: мигрировать всё сразу

Первый инстинкт — перенести всё за один день. Это не работает. Форматы данных в таблицах были хаотичными: даты в трёх разных форматах, суммы в рупиях и долларах вперемешку, имена гостей с опечатками, дубли с разным написанием. Попытка перенести всё сразу превратилась в трёхдневный кошмар очистки данных.

Правильный путь: начать с одного домена. Мы начали с бронирований. Настроили синхронизацию с eZee, убедились что данные консистентны, выстроили процессы — и только тогда перешли к финансам. Потом к задачам. Потом к лидам. Три недели вместо одного дня, зато без потерь.

Ошибка 2: откладывать индексы на потом

Первые недели база работала без продуманных индексов — «потом оптимизируем». Когда число запросов выросло до 50 000 в день, некоторые аналитические запросы начали занимать 3-5 секунд. После добавления нужных индексов — миллисекунды. Индексируйте с первого дня те поля, по которым делаете WHERE и JOIN.

Ошибка 3: не мониторить производительность запросов

Баг с 280 000 запросами в рассыльщике существовал несколько недель до того, как мы его заметили. Симптом — высокая нагрузка на сервер — был заметен, но причину искали долго. PostgreSQL встроенный инструмент pg_stat_statements показывает самые дорогие запросы — его нужно включать с самого начала. С ним мы бы нашли проблему за час, а не за пять дней.

Ошибка 4: давать агентам прямой доступ к базе

На начальном этапе несколько агентов подключались к базе напрямую с одним общим пользователем. Это неправильно по двум причинам: невозможно понять, кто сделал какой запрос, и один неправильный UPDATE может уронить данные. Решение — отдельный PostgreSQL-пользователь для каждого агента с минимально необходимыми правами. Агент для чтения лидов не должен иметь права писать в финансы.

Итоги: что изменилось после перехода на PostgreSQL

Прошло больше полугода с момента полного перехода на PostgreSQL. Вот конкретные изменения, которые можно измерить.

  • Скорость принятия решений: вопрос «сколько заработала вилла #12 в марте с разбивкой по каналам» раньше требовал 20-30 минут ручной работы. Сейчас — один SQL-запрос, три секунды.
  • Ноль рассинхронизаций: больше нет ситуаций, когда одна таблица показывает одно, другая — другое. Единый источник правды работает.
  • Автоматические задачи: триггеры создают задачи на уборку и встречу гостей автоматически. Менеджеры перестали забывать о плановых операциях.
  • Ранняя диагностика: мониторинг аномальных расходов поймал несколько случаев завышенного энергопотребления до того, как пришли счета. Экономия реальная.
  • Масштабируемость: добавление новой виллы — это один INSERT в таблицу villas. Все агенты, все отчёты, все дашборды автоматически начнут учитывать новый объект.
  • Контроль AI-расходов: благодаря таблице ai_logs точно знаем стоимость каждого агента. Оптимизация на основе реальных данных, а не ощущений.

280 000 запросов в день, 16 вилл, семь AI-агентов, два с половиной года операционных данных. PostgreSQL справляется. И это не предел — при необходимости можно добавить репликацию, read-реплики для аналитических запросов, Citus для горизонтального масштабирования.

Единая база данных — это не технический выбор. Это управленческий выбор. Выбор видеть бизнес целиком, а не через пять разных окошек одновременно. PostgreSQL дал нам именно это: одну точку правды для всего, что происходит в Solar Property.

Читайте также

Подписаться на блог в Telegram

Читайте свежие кейсы об AI-автоматизации, системной архитектуре и масштабировании бизнеса.

Подписаться