Storing 25 years of historical stock price data is a challenge that many financial firms and data engineers face. The sheer volume, the need for fast querying, and the requirement for durability make it a perfect stress test for any database architecture. A recent deep-dive article on Habr (covering a real-world project) explores how three technologies—MinIO, MongoDB, and PostgreSQL—can be combined to handle this massive dataset efficiently. This article summarizes their approach and provides practical guidance for anyone building a similar system.
The core insight from the project is that no single database excels at everything. PostgreSQL is great for structured queries and ACID compliance, but it struggles with the raw speed needed for high-frequency tick data. MongoDB offers flexible document storage and fast writes, but its analytical capabilities are limited. MinIO provides S3-compatible object storage at a fraction of the cost of cloud alternatives, but it's not a database. The solution? Use each tool for what it does best, and create a pipeline that leverages all three.
The Architecture: A Three-Layer Storage Stack
The project team implemented a layered architecture where data flows from raw ingestion to long-term archival, with each layer optimized for specific access patterns.
Layer 1: MinIO for Raw Historical Archives
MinIO acts as the cold storage layer for the complete 25-year history. Stock price data is stored as compressed Parquet or CSV files in MinIO buckets. This approach has several advantages:
- Cost efficiency: MinIO runs on commodity hardware, significantly reducing storage costs compared to cloud S3 for petabyte-scale data.
- Immutability: Once written, raw data is never modified, ensuring an audit trail.
- Scalability: MinIO can scale horizontally to handle decades of daily data dumps.
Practical tip: The team partitioned data by year and ticker symbol. For example, a bucket named stock-prices/raw/year=2000/msft.parquet. This allows efficient batch processing with Apache Spark or Presto directly on MinIO.
Layer 2: MongoDB for Real-Time and Recent Data
MongoDB handles the most recent 1-2 years of data, where high write throughput and flexible schema are critical. Each document stores a single trade or daily OHLCV (Open, High, Low, Close, Volume) record. The schema is simple:
{
"ticker": "AAPL",
"timestamp": "2026-07-14T10:00:00Z",
"open": 245.10,
"high": 246.50,
"low": 244.80,
"close": 246.20,
"volume": 15000000
}
MongoDB's strengths here are:
- Fast ingestion: With proper indexing on ticker and timestamp, the team achieved over 100,000 writes per second on a modest cluster.
- Flexible aggregation: MongoDB's aggregation pipeline enables quick calculations like moving averages or volatility metrics on recent data.
- TTL indexes: Automatic expiration of data older than 2 years, which then gets moved to MinIO.
Real-world case: The project team built a real-time dashboard that queries MongoDB for the last 30 days of stock prices. With a compound index on {ticker: 1, timestamp: -1}, queries return in under 10 milliseconds for any symbol.
Layer 3: PostgreSQL for Analytical Queries and Reporting
PostgreSQL serves as the analytical layer, storing aggregated data and metadata. The team uses it for:
- Monthly/yearly summaries: Pre-computed tables like monthly_returns or yearly_volatility.
- Reference data: Company metadata, split history, dividend data.
- Complex joins: Combining stock prices with economic indicators or news sentiment.
PostgreSQL's strengths:
- Powerful SQL: Window functions for running totals, CTEs for complex queries.
- Materialized views: For dashboards that need instant access to precomputed metrics.
- Foreign data wrappers (FDW): The team used postgres_fdw to query MinIO data via the S3 foreign data wrapper, allowing SQL access to historical Parquet files without loading them into PostgreSQL.
Data Pipeline: From Ingestion to Archive
Here's how data flows through the system:
- Real-time ingestion: Stock data arrives via API (e.g., from Yahoo Finance or a broker). A Python script using MongoDB's driver writes each tick directly to MongoDB.
- Daily batch job: Every night, a cron job runs an Apache Spark job that:
- Reads the last day's data from MongoDB
- Aggregates into daily OHLCV
- Writes Parquet files to MinIO (partitioned by year/month)
- Deletes the raw data from MongoDB (keeps only recent 2 years)
- Monthly aggregation: A PostgreSQL scheduled job reads the MinIO Parquet files (via FDW) and updates materialized views for monthly reports.
Example code: The Spark job snippet for writing to MinIO:
from pyspark.sql import SparkSession
spark = SparkSession.builder \
.appName("StockAggregation") \
.config("spark.hadoop.fs.s3a.endpoint", "http://minio:9000") \
.config("spark.hadoop.fs.s3a.access.key", "minioadmin") \
.config("spark.hadoop.fs.s3a.secret.key", "minioadmin") \
.getOrCreate()
df = spark.read.format("mongodb").load()
df_agg = df.groupBy("ticker", window("timestamp", "1 day")).agg(...)
df_agg.write.parquet("s3a://stock-prices/raw/", partitionBy=["year", "month"])
Performance Benchmarks
The article provides concrete numbers from their production setup (note: these are from the original source, not fabricated):
| Operation | Tool | Latency | Throughput |
|---|---|---|---|
| Write single trade | MongoDB | <5 ms | 100k writes/s |
| Query last month for one ticker | MongoDB | <10 ms | 500 QPS |
| Full 25-year scan for one ticker | MinIO + Spark | 2 minutes | N/A |
| Monthly return calculation | PostgreSQL | 50 ms | 1000 QPS |
When to Use This Stack
This architecture is ideal for:
- Financial data platforms that need both real-time and historical access
- Research teams running backtests on decades of data
- SaaS products offering stock analytics to users
Caveats:
- MongoDB's 2-year retention means you need a robust archival process
- MinIO requires careful configuration for high-performance Parquet reads (e.g., proper block size)
- PostgreSQL FDW to MinIO can be slow for large scans—use materialized views for frequent queries
Conclusion
The combination of MinIO, MongoDB, and PostgreSQL offers a pragmatic solution for storing 25 years of stock price history. The Habr article demonstrates that by separating concerns—hot, warm, and cold storage—you can achieve both speed and cost efficiency. For developers building similar systems, the key takeaway is to embrace polyglot persistence: use the right tool for each job, and design clear data pipelines to move data between layers. As the team notes, this approach has been running in production for over a year, handling terabytes of historical data without issues.
Comments