Разработка · 7 мин чтения

BI-дашборд с подключением к PostgreSQL на Vue: пошаговый разбор

Ко мне регулярно приходят с одной и той же задачей: данные лежат в PostgreSQL - заказы из WooCommerce, лиды с Tilda-форм, статусы посылок СДЭК - а руководителю нужен наглядный дашборд, а не выгрузка в Excel раз в неделю. BI дашборд с подключением к Postgres на Vue закрывает это через живой API поверх базы и компоненты визуализации во фронте. Разбираю весь путь: от архитектуры и прав доступа до графиков и обновления данных в реальном времени - так, как делаю это в реальных проектах.

Архитектура: почему Vue не подключается к PostgreSQL напрямую

Первый вопрос от клиентов - можно ли Vue-компоненту напрямую дёрнуть базу и вывести данные без бэкенда. Нет, и дело не в прихоти разработчика. Драйвер pg работает только в среде Node.js, а строка подключения с логином и паролем от боевой базы в браузерном бандле - готовая дыра для любого, кто откроет DevTools и посмотрит исходники бандла. Между фронтом и Postgres всегда стоит промежуточный слой: он держит соединение с базой и отдаёт наружу только JSON по HTTP или WebSocket.

На практике использую три варианта такого слоя, в зависимости от бюджета и сложности логики:

Вариант Время запуска Гибкость запросов Когда беру для проекта
Node.js/Express + pg 2-3 дня на базовый API Полная - любые агрегаты и джойны Дашборды со сложной бизнес-логикой и правами по ролям
PostgREST несколько часов Ограничена структурой таблиц и представлений Быстрый MVP или внутренний прототип
Supabase (self-hosted) около дня с настройкой Средняя, через RLS-политики Когда у команды уже есть инстанс Supabase под другие задачи

Для дашбордов с бизнес-логикой - расчёт конверсии, сведение данных из нескольких таблиц, права по ролям сотрудников - беру Express с сырыми SQL-запросами: полный контроль над тем, что уходит в базу и что возвращается на фронт. PostgREST хорош для внутреннего прототипа, когда за пару часов нужно показать заказчику, что данные вообще можно визуализировать. Supabase self-hosted имеет смысл, если у команды уже крутится инстанс под другие задачи - разворачивать его только ради одного дашборда избыточно.

Backend-слой на Node.js: пул соединений и REST-эндпоинты для дашборда

Бэкенд для дашборда не готовит десятки эндпоинтов на все случаи жизни - обычно хватает 5-8 маршрутов под конкретные виджеты: продажи по дням, топ товаров, воронка заказов, остатки по складам. Начинаю с пула соединений - создавать новый клиент pg на каждый запрос ошибка, которая укладывает базу при первой же нагрузке от нескольких открытых вкладок дашборда.

const { Pool } = require('pg');

const pool = new Pool({
  host: process.env.PG_HOST,
  port: 5432,
  user: process.env.PG_USER,
  password: process.env.PG_PASSWORD,
  database: process.env.PG_DATABASE,
  ssl: { rejectUnauthorized: false },
  max: 10,
  idleTimeoutMillis: 30000,
});

module.exports = pool;

Дальше - сами эндпоинты. Каждый принимает параметры фильтра (диапазон дат, филиал, категорию) и возвращает уже агрегированный результат, а не сырые строки для агрегации на фронте - Postgres считает суммы и группировки быстрее, чем JS в браузере.

app.get('/api/sales/daily', async (req, res) => {
  const { from, to } = req.query;
  const { rows } = await pool.query(
    `SELECT date_trunc('day', created_at) AS day, SUM(total) AS revenue
     FROM orders
     WHERE created_at BETWEEN $1 AND $2
     GROUP BY 1
     ORDER BY 1`,
    [from, to]
  );
  res.json(rows);
});

Пароль и хост базы храню в переменных окружения, .env в .gitignore, доступ к продовой базе - только через VPN или белый список IP. Для CORS разрешаю конкретный домен фронта, а не звёздочку.

Права доступа к PostgreSQL: read-only роль и защита подключения

Дашборд ничего не пишет в базу - только читает. Поэтому первое, что делаю на новом проекте: завожу отдельную роль с правами SELECT и подключаю бэкенд именно под ней, а не под основным пользователем приложения.

psql -U postgres -d shop -c "CREATE ROLE dashboard_ro LOGIN PASSWORD 'strong_password';"
psql -U postgres -d shop -c "GRANT CONNECT ON DATABASE shop TO dashboard_ro;"
psql -U postgres -d shop -c "GRANT USAGE ON SCHEMA public TO dashboard_ro;"
psql -U postgres -d shop -c "GRANT SELECT ON ALL TABLES IN SCHEMA public TO dashboard_ro;"
psql -U postgres -d shop -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO dashboard_ro;"

Плюс sslmode=require в строке подключения, если база не в одной приватной сети с сервером бэкенда, и закрытый порт 5432 наружу - Postgres слушает только localhost или адрес внутри VPC, наружу торчит исключительно HTTP от Node. Шаблоны таких SQL-скриптов под разные сценарии (read-only роль, отдельная схема для аналитики, ограничение по таблицам) собраны в подборке готовых скриптов - беру оттуда основу и адаптирую под структуру конкретной базы.

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

Компоненты BI-дашборда на Vue: графики, таблицы, фильтры

На фронте держу общую логику в Pinia-сторе: один раз запросил данные за период - переиспользую их между графиком, таблицей и карточками с итоговыми цифрами, вместо того чтобы дёргать API из каждого компонента отдельно.

<script setup>
import { ref, onMounted } from 'vue';
import * as echarts from 'echarts';
import axios from 'axios';

const chartRef = ref(null);

onMounted(async () => {
  const { data } = await axios.get('/api/sales/daily', {
    params: { from: '2026-06-01', to: '2026-07-01' },
  });

  const chart = echarts.init(chartRef.value);
  chart.setOption({
    xAxis: { type: 'category', data: data.map(d => d.day) },
    yAxis: { type: 'value' },
    series: [{ type: 'bar', data: data.map(d => d.revenue) }],
  });
});
</script>

Библиотеку графиков выбираю под задачу, а не по умолчанию:

Библиотека Размер бандла Гибкость Когда выбираю
ECharts около 1 MB Максимальная: тепловые карты, sankey, графы связей Сложные аналитические дашборды с десятком типов графиков
Chart.js около 200 KB Хватает на большинство типовых задач Простой дашборд с линиями, столбцами и пирогами
ApexCharts около 500 KB Хорошая анимация и зум из коробки Когда важен внешний вид без глубокой кастомизации

Для аналитических дашбордов с тепловыми картами, sankey-диаграммами воронки продаж или графами связей беру ECharts - тяжелее в бандле, зато не упираюсь в ограничения при усложнении визуализации на середине проекта. Для простого дашборда с парой линий и столбцов Chart.js закрывает задачу без лишнего веса.

Обновление данных в реальном времени: polling, WebSocket и LISTEN/NOTIFY

Не каждому дашборду нужен realtime - для еженедельной сводки хватает обновления раз в 5-10 минут через обычный setInterval с повторным запросом к API. Для операционных дашбордов (склад, статусы доставки СДЭК, очередь заказов) разница между «обновилось только что» и «обновится через пять минут» ощущается менеджером сразу.

Три уровня, от простого к сложному:

  • Polling - фронт сам раз в N секунд перезапрашивает данные. Просто, но лишняя нагрузка на базу при большом числе открытых вкладок.
  • WebSocket через Socket.io - бэкенд транслирует изменения всем подключённым клиентам, не дожидаясь их запроса.
  • LISTEN/NOTIFY в Postgres - база сама сообщает бэкенду о новой записи через триггер, а тот пробрасывает событие в сокет.
const { Client } = require('pg');
const client = new Client({ /* параметры подключения */ });

await client.connect();
await client.query('LISTEN orders_channel');

client.on('notification', (msg) => {
  io.emit('new-order', JSON.parse(msg.payload));
});

Похожий канал использовал на проекте с автоматизацией на n8n: тот же NOTIFY, что триггерит обновление дашборда, параллельно ловит aiogram-бот и присылает менеджеру алерт в Telegram, если выручка за день падает ниже порога - дашборд для наблюдения, бот для реакции, база одна.

Тяжёлые запросы: материализованные представления, индексы и стоимость проекта

Когда таблица заказов разрастается до пары миллионов строк - а это происходит быстрее, чем кажется на старте, особенно при интеграции с WooCommerce и СДЭК, - прямые агрегатные запросы на лету начинают выполняться по 8-12 секунд. Дашборд с таким временем отклика бесполезен: менеджер закрывает вкладку раньше, чем дождётся графика.

На одном из проектов решил это материализованным представлением: тяжёлая агрегация считается не при каждом открытии дашборда, а по расписанию - REFRESH MATERIALIZED VIEW раз в ночь через сценарий в n8n. Запрос с 8-12 секунд упал до 150-200 миллисекунд, потому что дашборд читает уже готовую витрину, а не пересчитывает джойны трёх таблиц на каждый клик фильтра. Плюс индексы на created_at и status - без них даже витрина фильтруется медленно на большом объёме.

Такой дашборд под ключ - бэкенд с правами доступа, витрины данных, графики на Vue и деплой - у меня стоит от 90 000 ₽, срок обычно 3-4 недели в зависимости от числа источников данных и сложности агрегаций. Дороже выходит не сама визуализация, а сведение данных из разных систем в единую структуру, если до этого они нигде не пересекались.

Интерфейс аналитики и отчётов

Дашборд / BI

от 90 000 ₽

Подробнее →

Частые вопросы

Можно ли использовать Supabase вместо своего бэкенда на Node.js?

Можно, если проект и так живёт в экосистеме Supabase - там уже есть auth, realtime из коробки и RLS-политики вместо ручных SQL-грантов. Для отдельного изолированного дашборда без остальной инфраструктуры Supabase разворачивать смысла обычно не вижу - Express с пулом соединений даёт тот же результат без лишнего сервиса на балансе.

Сколько данных выдерживает такой дашборд без тормозов?

На голых SQL-запросах без материализованных представлений комфортно держится таблица до 300-500 тысяч строк с индексами на нужных полях. После этого объёма закладываю витрины и кэш на уровне API - Redis на 1-5 минут для тяжёлых агрегатов, иначе каждый открытый дашборд превращается в отдельный тяжёлый запрос к базе.

Нужен ли выделенный сервер под бэкенд или хватит serverless-функций?

Для API поверх Postgres с пулом соединений постоянный процесс удобнее serverless - пул живёт между запросами, а не пересоздаётся на каждом холодном старте функции. Serverless имеет смысл только при низкой и редкой нагрузке, когда простаивающий сервер невыгоден.

Как быть, если данные лежат в нескольких базах Postgres одновременно?

Бэкенд держит отдельный pool на каждую базу и агрегирует результат уже на уровне Node перед отправкой на фронт, либо через dblink или foreign data wrapper на уровне самого Postgres, если базы в одной сети. Второй вариант проще поддерживать, но требует прав на настройку FDW на обеих сторонах.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Самозанятый Калинкин Н. А. · работаю с физлицами и юрлицами

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно