Every second, your system emits: CPU usage, memory, request count, error rate, latency percentiles, queue depth. Multiply by 200 services. That’s hundreds of thousands of data points per second, all append-only, all timestamped, and you mostly query them by time range.

Regular databases can handle this, technically. But they weren’t built for it.

What Makes Time-Series Different#

The access pattern is extreme. Writes are almost entirely appends: new data comes in, old data never changes. Reads are almost entirely range scans: “show me CPU for service-A from 2 PM to 3 PM.” Deletes happen in bulk: “drop everything older than 30 days.”

This is a perfect fit for LSM-tree based storage. Writes go to an in-memory buffer, flush to sorted files on disk. Range reads scan sequentially. No random I/O for writes, no write amplification from B-tree page splits.

Time-Based Partitioning#

Partition data by time window. This week’s data in one partition, last week’s in another. Benefits are enormous.

Writes always go to the latest partition. No contention with reads on old data. Range queries only scan relevant partitions. Deleting old data is just dropping a partition, instant, no row-by-row deletion.

-- MySQL partition by week
CREATE TABLE metrics (
    metric_name VARCHAR(255),
    timestamp BIGINT,
    value DOUBLE,
    tags JSON,
    PRIMARY KEY (metric_name, timestamp)
) PARTITION BY RANGE (timestamp) (
    PARTITION p_week_current VALUES LESS THAN (UNIX_TIMESTAMP('2026-03-23')),
    PARTITION p_week_prev VALUES LESS THAN (UNIX_TIMESTAMP('2026-03-16')),
    PARTITION p_week_old VALUES LESS THAN (UNIX_TIMESTAMP('2026-03-09'))
);
graph TD W["Incoming Metrics"] --> CP["Current Partition (this week)"] R["Range Query: last 2 hours"] --> CP D["Retention: drop data > 30 days"] --> OP["Old Partition"] OP --> DROP["DROP PARTITION (instant)"] style W fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style CP fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style R fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style D fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style OP fill:#000000,stroke:#ff0000,stroke-width:2px,color:#fff style DROP fill:#000000,stroke:#ff0000,stroke-width:2px,color:#fff

Compression#

Consecutive metric values are often similar. CPU was 42%, then 43%, then 42%, then 44%. Delta encoding stores the differences: 42, +1, -1, +2. Much smaller than storing full values. Timestamps are even better: if you sample every 10 seconds, store the start time and the interval once, not every timestamp.

This is why dedicated time-series databases (InfluxDB, Prometheus) achieve 10-20x better compression than general-purpose databases on the same data.

When MySQL Is Enough#

Honestly, for many teams MySQL with time-based partitioning handles metrics just fine. I’ve seen it work for up to a few thousand metrics per second. You don’t need a specialized time-series database until your ingestion rate makes partition management and query performance painful.

At Oracle, we built a performance monitoring dashboard for NSSF config operations. Every config change logged: timestamp, operation type, duration, success/failure. Modest volume, maybe 100 events per second. We stored it in MySQL with partitions by day. Queries like “average config time for the last 24 hours” ran fine. The 95% reduction in config time (from the auto-config system) was visible directly in these time-series queries: a clear before/after step function in the graph. Partitioning by day made retention simple: a cron job dropped partitions older than 90 days.

What I’m Learning#

Time-series data has one of the most predictable access patterns in all of computing: append, range scan, bulk delete. That predictability is what makes specialized storage work so well. The connection to write-ahead logging and LSM-trees is direct: both optimize for sequential writes. If your metrics volume is modest, MySQL partitions work. If it’s massive, the specialized engines earn their keep through compression and purpose-built query engines.

What do you use for storing metrics, and at what scale did you outgrow a general-purpose database?