← Назад к вопросам

Как спроектировать бд с парковочными зонами в Москве с нуля?

2.3 Middle🔥 111 комментариев
#SQL и базы данных

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

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как спроектировать БД с парковочными зонами в Москве с нуля?

Дизайн базы данных для системы паркировки требует учёта географических данных, real-time обновлений, аналитики и масштабируемости. Это типичный пример для geo-spatial приложения.

1. Анализ требований

Функциональные требования:

  • Хранение границ парковочных зон (полигоны)
  • Отслеживание доступных мест (real-time)
  • История паркирования (аналитика)
  • Тарифы и правила для каждой зоны
  • Поиск ближайшей свободной зоны по GPS

Нефункциональные требования:

  • Высокая доступность (99.9%+)
  • Latency < 500ms для geo-queries
  • Масштабируемость на миллионы парковочных мест
  • Аналитика в real-time

2. Схема базы данных (PostgreSQL + PostGIS)

-- Создаём расширение для работы с географией
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS pg_trgm;  -- для полнотекстового поиска

-- Таблица парковочных зон
CREATE TABLE parking_zones (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    zone_name VARCHAR(255) NOT NULL,
    zone_code VARCHAR(50) UNIQUE NOT NULL,
    polygon GEOMETRY(POLYGON, 4326) NOT NULL,  -- WGS84
    district VARCHAR(100),  -- СВАО, ЮЗАО, etc.
    zone_type VARCHAR(50),  -- 'standard', 'residents', 'commercial'
    
    -- Мощность
    total_spaces INT NOT NULL,
    reserved_spaces INT DEFAULT 0,  -- для инвалидов, жителей
    
    -- Тарифы (рубли за час)
    rate_per_hour DECIMAL(10, 2),
    max_daily_rate DECIMAL(10, 2),
    
    -- Режим работы
    work_hours_start TIME,
    work_hours_end TIME,
    week_days VARCHAR(100),  -- 'Mon,Tue,Wed,Thu,Fri,Sat,Sun'
    
    -- Ограничения
    max_parking_duration_hours INT,
    free_parking_for_residents BOOLEAN DEFAULT FALSE,
    
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_parking_zones_polygon ON parking_zones USING GIST(polygon);
CREATE INDEX idx_parking_zones_district ON parking_zones(district);
CREATE INDEX idx_parking_zones_type ON parking_zones(zone_type);

-- Таблица парковочных мест
CREATE TABLE parking_spaces (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    zone_id UUID NOT NULL REFERENCES parking_zones(id) ON DELETE CASCADE,
    space_number VARCHAR(50) NOT NULL,
    position GEOMETRY(POINT, 4326) NOT NULL,  -- GPS координаты
    
    -- Статус
    is_occupied BOOLEAN DEFAULT FALSE,
    is_available BOOLEAN DEFAULT TRUE,
    is_disabled BOOLEAN DEFAULT FALSE,
    
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
    UNIQUE(zone_id, space_number)
);

CREATE INDEX idx_parking_spaces_zone ON parking_spaces(zone_id);
CREATE INDEX idx_parking_spaces_position ON parking_spaces USING GIST(position);
CREATE INDEX idx_parking_spaces_occupied ON parking_spaces(is_occupied, zone_id);

-- Таблица сессий паркирования
CREATE TABLE parking_sessions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL,
    space_id UUID NOT NULL REFERENCES parking_spaces(id),
    zone_id UUID NOT NULL REFERENCES parking_zones(id),
    
    -- Время
    start_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
    end_time TIMESTAMP WITH TIME ZONE,
    
    -- Платёж
    expected_cost DECIMAL(10, 2),
    actual_cost DECIMAL(10, 2),
    paid BOOLEAN DEFAULT FALSE,
    
    -- Место входа/выхода
    entry_point GEOMETRY(POINT, 4326),
    exit_point GEOMETRY(POINT, 4326),
    
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_parking_sessions_user ON parking_sessions(user_id);
CREATE INDEX idx_parking_sessions_zone ON parking_sessions(zone_id);
CREATE INDEX idx_parking_sessions_start_time ON parking_sessions(start_time DESC);
CREATE INDEX idx_parking_sessions_status ON parking_sessions(end_time) WHERE end_time IS NULL;  -- активные

-- Таблица для аналитики (денормализованная)
CREATE TABLE parking_stats_hourly (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    zone_id UUID NOT NULL REFERENCES parking_zones(id),
    hour_timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
    
    -- Метрики
    occupied_count INT,
    available_count INT,
    occupancy_rate DECIMAL(5, 2),  -- процент
    avg_stay_duration_minutes INT,
    revenue DECIMAL(15, 2),
    
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
    UNIQUE(zone_id, hour_timestamp)
);

CREATE INDEX idx_parking_stats_zone_time ON parking_stats_hourly(zone_id, hour_timestamp DESC);

3. Оптимизация для geo-queries

-- Поиск ближайшей свободной парковки по GPS
PREPARE find_nearest_parking AS
SELECT 
    z.id,
    z.zone_name,
    z.rate_per_hour,
    COUNT(CASE WHEN ps.is_occupied = FALSE THEN 1 END) as available_spaces,
    ST_Distance(
        z.polygon::geography,
        ST_PointFromText('POINT($1 $2)', 4326)::geography
    ) / 1000 as distance_km
FROM parking_zones z
LEFT JOIN parking_spaces ps ON z.id = ps.zone_id
WHERE ST_DWithin(
    z.polygon::geography,
    ST_PointFromText('POINT($1 $2)', 4326)::geography,
    5000  -- 5км радиус
)
AND COUNT(CASE WHEN ps.is_occupied = FALSE THEN 1 END) > 0
GROUP BY z.id
ORDER BY distance_km
LIMIT 10;

-- Поиск зон по полигону (пользователь паркуется где-то, найти его зону)
PREPARE find_zone_by_location AS
SELECT z.*, 
    ST_Area(z.polygon::geography) / 10000 as area_hectares
FROM parking_zones z
WHERE ST_Contains(z.polygon, ST_PointFromText('POINT($1 $2)', 4326));

4. Python-скрипт для работы с БД

from sqlalchemy import create_engine, text
from sqlalchemy.orm import Session
import geopandas as gpd
from shapely.geometry import Point, Polygon
import json

class ParkingDatabase:
    def __init__(self, connection_string):
        self.engine = create_engine(connection_string)
    
    def find_nearest_zones(self, latitude, longitude, radius_km=5):
        """Найти ближайшие свободные зоны"""
        query = text("""
        SELECT 
            z.id,
            z.zone_name,
            z.rate_per_hour,
            COUNT(CASE WHEN ps.is_occupied = FALSE THEN 1 END) as available_spaces,
            ST_Distance(
                z.polygon::geography,
                ST_PointFromText('POINT(:lon :lat)', 4326)::geography
            ) / 1000 as distance_km
        FROM parking_zones z
        LEFT JOIN parking_spaces ps ON z.id = ps.zone_id
        WHERE ST_DWithin(
            z.polygon::geography,
            ST_PointFromText('POINT(:lon :lat)', 4326)::geography,
            :radius
        )
        GROUP BY z.id
        HAVING COUNT(CASE WHEN ps.is_occupied = FALSE THEN 1 END) > 0
        ORDER BY distance_km
        LIMIT 10
        """)
        
        with self.engine.connect() as conn:
            results = conn.execute(query, {
                'lat': latitude,
                'lon': longitude,
                'radius': radius_km * 1000
            })
            return [dict(row) for row in results]
    
    def reserve_space(self, user_id, space_id):
        """Зарезервировать парковочное место"""
        query = text("""
        INSERT INTO parking_sessions (user_id, space_id, zone_id, start_time)
        SELECT :user_id, :space_id, ps.zone_id, CURRENT_TIMESTAMP
        FROM parking_spaces ps
        WHERE ps.id = :space_id AND ps.is_occupied = FALSE
        RETURNING id
        """)
        
        with self.engine.begin() as conn:
            result = conn.execute(query, {
                'user_id': user_id,
                'space_id': space_id
            })
            return result.scalar()
    
    def get_occupancy_rate(self, zone_id, hours=24):
        """Получить % занятости за последние N часов"""
        query = text("""
        SELECT 
            hour_timestamp,
            occupancy_rate,
            available_count
        FROM parking_stats_hourly
        WHERE zone_id = :zone_id
            AND hour_timestamp >= CURRENT_TIMESTAMP - INTERVAL ':hours hours'
        ORDER BY hour_timestamp DESC
        """)
        
        with self.engine.connect() as conn:
            results = conn.execute(query, {
                'zone_id': zone_id,
                'hours': hours
            })
            return [dict(row) for row in results]

# Использование
db = ParkingDatabase("postgresql://user:pass@localhost/parking_db")

# Найти свободные парковки
zones = db.find_nearest_zones(latitude=55.7558, longitude=37.6173)  # Красная площадь
for zone in zones:
    print(f"{zone['zone_name']}: {zone['available_spaces']} мест на расстоянии {zone['distance_km']:.2f}км")

# Процент занятости
stats = db.get_occupancy_rate(zone_id="uuid-here", hours=24)

5. Структура для аналитики и отчётов

-- Дневной отчёт по выручке по зонам
CREATE VIEW daily_revenue_by_zone AS
SELECT 
    z.zone_name,
    z.district,
    DATE(ps.start_time) as date,
    COUNT(DISTINCT ps.id) as sessions,
    SUM(ps.actual_cost) as total_revenue,
    AVG(ps.actual_cost) as avg_revenue,
    AVG(EXTRACT(EPOCH FROM (ps.end_time - ps.start_time)) / 3600) as avg_duration_hours
FROM parking_sessions ps
JOIN parking_zones z ON ps.zone_id = z.id
WHERE ps.paid = TRUE
GROUP BY z.zone_name, z.district, DATE(ps.start_time)
ORDER BY date DESC;

-- Пиковые часы по зонам
CREATE VIEW peak_hours_by_zone AS
SELECT 
    zone_id,
    DATE_TRUNC('hour', start_time)::TIME as hour,
    COUNT(*) as session_count,
    AVG(EXTRACT(EPOCH FROM (end_time - start_time)) / 3600) as avg_duration
FROM parking_sessions
GROUP BY zone_id, DATE_TRUNC('hour', start_time)
ORDER BY session_count DESC;

6. Масштабирование

  • Реплика для чтения: часто запрашиваемые geo-данные
  • Шардинг по районам: разные БД для разных округов Москвы
  • Redis кеш: часто используемые зоны
  • TimescaleDB: для временных рядов аналитики
  • Elasticsearch: для поиска по названиям зон

Эта архитектура обеспечивает масштабируемость, точность и эффективность работы с паркировочной системой.

Как спроектировать бд с парковочными зонами в Москве с нуля? | PrepBro