Why MySQL for these platforms
The EV and vending platforms write constantly — sessions, telemetry, status changes — and read in narrow, predictable patterns. InnoDB handles that write load gracefully, managed MySQL on Azure keeps the ops surface near zero, and the cost profile beats commercial licenses by an order of magnitude at fleet scale. For platforms sold to cost-sensitive enterprise clients, that last point is architecture, not accounting.
Schema decisions that mattered
- Time-series discipline for telemetry: partitioned tables by month, hot data indexed tightly, older partitions archived to cheap storage — meter values are append-only, treat them that way.
- UUIDs vs auto-increment, chosen deliberately: ordered UUIDs for anything partner-visible (OCPI object IDs), auto-increment for internal high-volume rows where index locality matters.
- Strict mode always: silent truncation is a data-corruption feature; every platform runs with strict SQL mode from day one.
- Charset utf8mb4 everywhere: learned once, the hard way, via a partner name with an emoji.
Operating it at platform scale
- Read replicas for reporting and partner queries — the OCPI locations pull from a CPO never competes with session writes.
- Connection pooling tuned per service; .NET’s pool defaults plus 10 microservices can exhaust max_connections quietly.
- Slow query log reviewed weekly as a team habit, not a crisis response.
- Backups are only real if restore is tested — we restore into staging monthly and time it.
Worked example: telemetry table built for scale
Append-only meter values, partitioned by month, with the rollup that answers every dashboard question:
CREATE TABLE meter_value (
charger_id BINARY(16) NOT NULL,
session_id BINARY(16) NOT NULL,
measured_at DATETIME(3) NOT NULL,
energy_wh INT UNSIGNED NOT NULL,
power_w INT UNSIGNED NOT NULL,
PRIMARY KEY (charger_id, measured_at, session_id)
) ENGINE=InnoDB
PARTITION BY RANGE COLUMNS (measured_at) (
PARTITION p2026_07 VALUES LESS THAN ('2026-08-01'),
PARTITION p2026_08 VALUES LESS THAN ('2026-09-01'),
PARTITION p_future VALUES LESS THAN (MAXVALUE)
);
-- hourly rollup keeps dashboards off the raw table
INSERT INTO energy_hourly (charger_id, hour_start, kwh)
SELECT charger_id, DATE_FORMAT(measured_at, '%Y-%m-%d %H:00:00'),
(MAX(energy_wh) - MIN(energy_wh)) / 1000
FROM meter_value
WHERE measured_at >= NOW() - INTERVAL 1 HOUR
GROUP BY charger_id, 2
ON DUPLICATE KEY UPDATE kwh = VALUES(kwh);
Dropping a two-year-old partition is instant; deleting the same rows would lock the table for an hour.
FAQ
Both excellent. MySQL won here on managed-service maturity in the client’s cloud estate and team familiarity. PostgreSQL’s richer types and indexing are real advantages — evaluate per platform, not per fashion.
Monthly partitions, append-only writes, aggressive archival, and pre-aggregated rollup tables for dashboards. The raw table answers audits; the rollups answer every daily question.
Not yet needed — vertical headroom plus read replicas plus partitioning carries further than most teams expect. Shard when a single primary genuinely cannot absorb the write load, not before.