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.